diff --git a/.changeset/@graphql-tools_documents-8364-dependencies.md b/.changeset/@graphql-tools_documents-8364-dependencies.md deleted file mode 100644 index de577b3ac65..00000000000 --- a/.changeset/@graphql-tools_documents-8364-dependencies.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@graphql-tools/documents": patch ---- -dependencies updates: - - Removed dependency [`lodash.sortby@^4.7.0` ↗︎](https://www.npmjs.com/package/lodash.sortby/v/4.7.0) (from `dependencies`) diff --git a/.changeset/mock-resolver-validation-options.md b/.changeset/mock-resolver-validation-options.md new file mode 100644 index 00000000000..ec0abba1f64 --- /dev/null +++ b/.changeset/mock-resolver-validation-options.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/mock': patch +--- + +Pass `resolverValidationOptions` through from `addMocksToSchema` to `addResolversToSchema`. diff --git a/.changeset/optimize-remove-descriptions-executable.md b/.changeset/optimize-remove-descriptions-executable.md new file mode 100644 index 00000000000..699bac74dfe --- /dev/null +++ b/.changeset/optimize-remove-descriptions-executable.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/optimize': patch +--- + +Remove descriptions from operation, variable, fragment, schema definition, and schema extension nodes in `removeDescriptions`. diff --git a/.changeset/utils-isvalidpath-percent.md b/.changeset/utils-isvalidpath-percent.md new file mode 100644 index 00000000000..959bef9ae34 --- /dev/null +++ b/.changeset/utils-isvalidpath-percent.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/utils': patch +--- + +Allow `%` in paths checked by `isValidPath` (e.g. directories from URL-encoded repo names). diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 83da7a9c67b..077234ab475 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,13 +79,12 @@ jobs: run: npx bob check test: name: - ${{matrix.name}} Test on Node ${{matrix.node-version}} (${{matrix.os}}) and GraphQL + Test on Node ${{matrix.node-version}} (${{matrix.os}}) and GraphQL v${{matrix.graphql_version}} runs-on: ${{matrix.os}} strategy: fail-fast: false matrix: - name: [Unit, Leak] os: [windows-latest, ubuntu-latest] # remove windows to speed up the tests node-version: [18, 22, 24] graphql_version: @@ -128,12 +127,19 @@ jobs: ${{ runner.os }}-${{matrix.node-version}}-${{matrix.graphql_version}}-jest- - name: Build run: npm run build - - name: ${{matrix.name}} Tests - uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 - with: - timeout_minutes: 10 - max_attempts: 5 - command: npm run ${{matrix.name == 'Leak' && 'test:leaks' || 'test'}} --ci + - parallel: + - name: Unit Tests + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 + with: + timeout_minutes: 10 + max_attempts: 5 + command: npm run test --ci + - name: Leak Tests + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 + with: + timeout_minutes: 10 + max_attempts: 5 + command: npm run test:leaks --ci test-bun: name: Unit Test on Bun diff --git a/packages/documents/src/sort-executable-nodes.ts b/packages/documents/src/sort-executable-nodes.ts index 1e0a2b58701..602ea88235b 100644 --- a/packages/documents/src/sort-executable-nodes.ts +++ b/packages/documents/src/sort-executable-nodes.ts @@ -76,13 +76,25 @@ export function sortExecutableNodes( } return cacheResult( - [...nodes].sort((a, b) => { - const kindComparison = compareKeys(a.kind, b.kind); - if (kindComparison !== 0) { - return kindComparison; - } - return compareKeys(getNodeNameValue(a), getNodeNameValue(b)); - }), + nodes + .map((node, index) => ({ + node, + index, + kind: node.kind, + name: getNodeNameValue(node), + })) + .sort((a, b) => { + const kindComparison = compareKeys(a.kind, b.kind); + if (kindComparison !== 0) { + return kindComparison; + } + const nameComparison = compareKeys(a.name, b.name); + if (nameComparison !== 0) { + return nameComparison; + } + return a.index - b.index; + }) + .map(item => item.node), ); } } @@ -106,7 +118,16 @@ function sortNodesByStringKey( nodes: readonly TNode[], getKey: (node: TNode) => string | undefined, ): readonly TNode[] { - return [...nodes].sort((a, b) => compareKeys(getKey(a), getKey(b))); + return nodes + .map((node, index) => ({ node, index, key: getKey(node) })) + .sort((a, b) => { + const keyComparison = compareKeys(a.key, b.key); + if (keyComparison !== 0) { + return keyComparison; + } + return a.index - b.index; + }) + .map(item => item.node); } function compareKeys(a: string | undefined, b: string | undefined): number { diff --git a/packages/mock/src/addMocksToSchema.ts b/packages/mock/src/addMocksToSchema.ts index 2d1eef8ee73..dddb184ed42 100644 --- a/packages/mock/src/addMocksToSchema.ts +++ b/packages/mock/src/addMocksToSchema.ts @@ -9,7 +9,12 @@ import { isUnionType, } from 'graphql'; import { addResolversToSchema } from '@graphql-tools/schema'; -import { IResolvers, MapperKind, mapSchema } from '@graphql-tools/utils'; +import { + IResolvers, + IResolverValidationOptions, + MapperKind, + mapSchema, +} from '@graphql-tools/utils'; import { createMockStore } from './MockStore.js'; import { IMocks, IMockStore, isRef, MockGenerationBehavior, TypePolicy } from './types.js'; import { copyOwnProps, isObject, isRootType } from './utils.js'; @@ -29,6 +34,11 @@ type IMockOptions = { * server and not others. */ preserveResolvers?: boolean; + /** + * Additional options for validating the provided resolvers. + * Passed through to `addResolversToSchema`. + */ + resolverValidationOptions?: IResolverValidationOptions; }; // todo: add option to preserve resolver @@ -98,6 +108,7 @@ export function addMocksToSchema({ typePolicies, resolvers: resolversOrFnResolvers, preserveResolvers = false, + resolverValidationOptions, }: IMockOptions): GraphQLSchema { if (!schema) { throw new Error('Must provide schema to mock'); @@ -258,6 +269,7 @@ export function addMocksToSchema({ resolvers: resolvers as any, // This option ensures that schemas are not cloned multiple times, which can be very expensive updateResolversInPlace: true, + resolverValidationOptions, }) : schemaWithMocks; } diff --git a/packages/mock/tests/addMocksToSchema.spec.ts b/packages/mock/tests/addMocksToSchema.spec.ts index 1d71a1c9336..50a65d6d4eb 100644 --- a/packages/mock/tests/addMocksToSchema.spec.ts +++ b/packages/mock/tests/addMocksToSchema.spec.ts @@ -491,6 +491,33 @@ describe('addMocksToSchema', () => { expect(viewer.name).toEqual('custom mock for String'); }); + it('passes resolverValidationOptions through to addResolversToSchema', () => { + expect(() => + addMocksToSchema({ + schema, + resolvers: { + Query: { + doesNotExist: () => null, + }, + }, + }), + ).toThrow(/defined in resolvers, but not in schema/); + + expect(() => + addMocksToSchema({ + schema, + resolvers: { + Query: { + doesNotExist: () => null, + }, + }, + resolverValidationOptions: { + requireResolversToMatchSchema: 'ignore', + }, + }), + ).not.toThrow(); + }); + it('creates a new schema whether or not resolvers are passed in', () => { expect( Object.is( diff --git a/packages/optimize/src/optimizers/remove-description.ts b/packages/optimize/src/optimizers/remove-description.ts index ca72f986da6..5fbb566136b 100644 --- a/packages/optimize/src/optimizers/remove-description.ts +++ b/packages/optimize/src/optimizers/remove-description.ts @@ -2,16 +2,17 @@ import { visit } from 'graphql'; import { DocumentOptimizer } from '../types.js'; /** - * This optimizer removes "description" field from schema AST definitions. + * This optimizer removes "description" fields from schema and executable AST nodes. * @param input */ export const removeDescriptions: DocumentOptimizer = input => { function transformNode(node: any) { - if (node.description) { - node.description = undefined; + if (!node.description) { + return node; } - return node; + const { description, ...rest } = node; + return rest; } return visit(input, { @@ -25,5 +26,10 @@ export const removeDescriptions: DocumentOptimizer = input => { InputValueDefinition: transformNode, FieldDefinition: transformNode, DirectiveDefinition: transformNode, + OperationDefinition: transformNode, + VariableDefinition: transformNode, + FragmentDefinition: transformNode, + SchemaDefinition: transformNode, + SchemaExtension: transformNode, }); }; diff --git a/packages/optimize/tests/remove-description.spec.ts b/packages/optimize/tests/remove-description.spec.ts index b9b85338df8..672d76aed1d 100644 --- a/packages/optimize/tests/remove-description.spec.ts +++ b/packages/optimize/tests/remove-description.spec.ts @@ -106,4 +106,58 @@ scalar TestScalar `.trim(), ); }); + + it('should remove descriptions from operations, variables, and fragments', () => { + // Attach description nodes manually so the fixture works on GraphQL versions + // that cannot parse executable descriptions in SDL (e.g. graphql@15). + const doc = parse(/* GraphQL */ ` + query user($id: ID!) { + user(id: $id) { + ...userFields + } + } + + fragment userFields on User { + id + username + } + `); + + const operation = doc.definitions[0] as any; + const fragment = doc.definitions[1] as any; + operation.description = { + kind: 'StringValue', + value: 'OPERATION DESCRIPTION', + block: true, + }; + operation.variableDefinitions[0].description = { + kind: 'StringValue', + value: 'VARIABLE DESCRIPTION', + block: true, + }; + fragment.description = { + kind: 'StringValue', + value: 'FRAGMENT DESCRIPTION', + block: true, + }; + + const out = removeDescriptions(doc); + expect((out.definitions[0] as any).description).toBeUndefined(); + expect((out.definitions[0] as any).variableDefinitions[0].description).toBeUndefined(); + expect((out.definitions[1] as any).description).toBeUndefined(); + expect(print(out).trim()).toBe( + /* GraphQL */ ` +query user($id: ID!) { + user(id: $id) { + ...userFields + } +} + +fragment userFields on User { + id + username +} + `.trim(), + ); + }); }); diff --git a/packages/utils/src/helpers.ts b/packages/utils/src/helpers.ts index b91d96ff021..c5d5653f912 100644 --- a/packages/utils/src/helpers.ts +++ b/packages/utils/src/helpers.ts @@ -78,9 +78,9 @@ export function isDocumentString(str: any): boolean { return false; } -const invalidPathRegex = /[‘“!%^<>`\n]/; +const invalidPathRegex = /[‘“!^<>`\n]/; /** - * Checkes whether the `str` contains any path illegal characters. + * Checks whether the `str` contains any path illegal characters. * * A string may sometimes look like a path but is not (like an SDL of a simple * GraphQL schema). To make sure we don't yield false-positives in such cases, diff --git a/packages/utils/tests/helpers.test.ts b/packages/utils/tests/helpers.test.ts index f5c36f3e99e..34b6104025a 100644 --- a/packages/utils/tests/helpers.test.ts +++ b/packages/utils/tests/helpers.test.ts @@ -24,12 +24,17 @@ describe('helpers', () => { expect(isValidPath(str)).toBeFalsy(); }); - it.each(['file', 'file.tsx', 'some/where/file.tsx', '/some/where/file.tsx'])( - 'should detect "%s" as a valid path', - str => { - expect(isValidPath(str)).toBeTruthy(); - }, - ); + it.each([ + 'file', + 'file.tsx', + 'some/where/file.tsx', + '/some/where/file.tsx', + 'Repo%20Name/src/App.tsx', + 'C:/dev/Repo%20Name/project/src/App.tsx', + 'src/invalid%20path/InvalidFile.ts', + ])('should detect "%s" as a valid path', str => { + expect(isValidPath(str)).toBeTruthy(); + }); }); describe('isUrl', () => {