Skip to content
Merged
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
5 changes: 0 additions & 5 deletions .changeset/@graphql-tools_documents-8364-dependencies.md

This file was deleted.

5 changes: 5 additions & 0 deletions .changeset/mock-resolver-validation-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/mock': patch
---

Pass `resolverValidationOptions` through from `addMocksToSchema` to `addResolversToSchema`.
5 changes: 5 additions & 0 deletions .changeset/optimize-remove-descriptions-executable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/optimize': patch
---

Remove descriptions from operation, variable, fragment, schema definition, and schema extension nodes in `removeDescriptions`.
5 changes: 5 additions & 0 deletions .changeset/utils-isvalidpath-percent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/utils': patch
---

Allow `%` in paths checked by `isValidPath` (e.g. directories from URL-encoded repo names).
22 changes: 14 additions & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test-bun:
name: Unit Test on Bun
Expand Down
37 changes: 29 additions & 8 deletions packages/documents/src/sort-executable-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
}
}
Expand All @@ -106,7 +118,16 @@ function sortNodesByStringKey<TNode extends ASTNode>(
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 {
Expand Down
14 changes: 13 additions & 1 deletion packages/mock/src/addMocksToSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,6 +34,11 @@ type IMockOptions<TResolvers = IResolvers> = {
* 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
Expand Down Expand Up @@ -98,6 +108,7 @@ export function addMocksToSchema<TResolvers = IResolvers>({
typePolicies,
resolvers: resolversOrFnResolvers,
preserveResolvers = false,
resolverValidationOptions,
}: IMockOptions<TResolvers>): GraphQLSchema {
if (!schema) {
throw new Error('Must provide schema to mock');
Expand Down Expand Up @@ -258,6 +269,7 @@ export function addMocksToSchema<TResolvers = IResolvers>({
resolvers: resolvers as any,
// This option ensures that schemas are not cloned multiple times, which can be very expensive
updateResolversInPlace: true,
resolverValidationOptions,
})
: schemaWithMocks;
}
27 changes: 27 additions & 0 deletions packages/mock/tests/addMocksToSchema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
14 changes: 10 additions & 4 deletions packages/optimize/src/optimizers/remove-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Comment thread
ardatan marked this conversation as resolved.
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, {
Expand All @@ -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,
});
};
54 changes: 54 additions & 0 deletions packages/optimize/tests/remove-description.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);
});
Comment thread
ardatan marked this conversation as resolved.
});
4 changes: 2 additions & 2 deletions packages/utils/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ export function isDocumentString(str: any): boolean {
return false;
}

const invalidPathRegex = /[‘“!%^<>`\n]/;
const invalidPathRegex = /[‘“!^<>`\n]/;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/**
* Checkes whether the `str` contains any path illegal characters.
* Checks whether the `str` contains any path illegal characters.
*
Comment thread
Copilot marked this conversation as resolved.
* 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,
Expand Down
17 changes: 11 additions & 6 deletions packages/utils/tests/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading