Skip to content

Commit 10db869

Browse files
committed
fix(generate): harmonize validators
see #2103
1 parent 71df0ae commit 10db869

File tree

10 files changed

+128
-90
lines changed

10 files changed

+128
-90
lines changed

.changeset/weak-peaches-count.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
'@graphql-tools/batch-execute': major
33
'@graphql-tools/delegate': major
4+
'@graphql-tools/generate': major
45
'@graphql-tools/mock': major
56
'@graphql-tools/stitch': major
67
'@graphql-tools/utils': major
@@ -12,6 +13,12 @@
1213

1314
## Breaking Changes:
1415

16+
#### Schema Generation and Decoration API (`@graphql-tools/schema`)
17+
18+
- Resolver validation options should now be set to `error`, `warn` or `ignore` rather than `true` or `false`. In previous versions, some of the validators caused errors to be thrown, while some issued warnings. This changes brings consistency to validator behavior.
19+
20+
- The `allowResolversNotInSchema` has been renamed to `requireResolversToMatchSchema`, to harmonize the naming convention of all the validators. The default setting of `requireResolversToMatchSchema` is `error`, matching the previous behavior.
21+
1522
#### Schema Delegation (`delegateToSchema` & `@graphql-tools/delegate`)
1623

1724
- The `delegateToSchema` return value has matured and been formalized as an `ExternalObject`, in which all errors are integrated into the GraphQL response, preserving their initial path. Those advanced users accessing the result directly will note the change in error handling. This also allows for the deprecation of unnecessary helper functions including `slicedError`, `getErrors`, `getErrorsByPathSegment` functions. Only external errors with missing or invalid paths must still be preserved by annotating the remote object with special properties. The new `getUnpathedErrors` function is therefore necessary for retrieving only these errors. Note also the new `annotateExternalObject` and `mergeExternalObjects` functions, as well as the renaming of `handleResult` to `resolveExternalValue`.

packages/merge/src/merge-schemas.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ export interface MergeSchemasConfig<Resolvers extends IResolvers = IResolvers> e
4242
}
4343

4444
const defaultResolverValidationOptions: Partial<IResolverValidationOptions> = {
45-
requireResolversForArgs: false,
46-
requireResolversForNonScalar: false,
47-
requireResolversForAllFields: false,
48-
requireResolversForResolveType: false,
49-
allowResolversNotInSchema: true,
45+
requireResolversForArgs: 'ignore',
46+
requireResolversForNonScalar: 'ignore',
47+
requireResolversForAllFields: 'ignore',
48+
requireResolversForResolveType: 'ignore',
49+
requireResolversToMatchSchema: 'ignore',
5050
};
5151

5252
/**

packages/mock/tests/mocking.spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -983,11 +983,6 @@ describe('Mock', () => {
983983
let jsSchema = makeExecutableSchema({
984984
typeDefs: [shorthand],
985985
resolvers,
986-
resolverValidationOptions: {
987-
requireResolversForArgs: false,
988-
requireResolversForNonScalar: false,
989-
requireResolversForAllFields: false,
990-
},
991986
logger: console,
992987
});
993988
const mockMap = {

packages/schema/src/addResolversToSchema.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function addResolversToSchema(
5555
updateResolversInPlace = false,
5656
} = options;
5757

58-
const { allowResolversNotInSchema = false, requireResolversForResolveType } = resolverValidationOptions;
58+
const { requireResolversToMatchSchema = 'error', requireResolversForResolveType } = resolverValidationOptions;
5959

6060
const resolvers = inheritResolversFromInterfaces
6161
? extendResolversFromInterfaces(schema, inputResolvers)
@@ -85,7 +85,7 @@ export function addResolversToSchema(
8585
const type = schema.getType(typeName);
8686

8787
if (type == null) {
88-
if (allowResolversNotInSchema) {
88+
if (requireResolversToMatchSchema === 'ignore') {
8989
return;
9090
}
9191

@@ -106,14 +106,19 @@ export function addResolversToSchema(
106106
if (
107107
!fieldName.startsWith('__') &&
108108
!values.some(value => value.name === fieldName) &&
109-
!allowResolversNotInSchema
109+
requireResolversToMatchSchema &&
110+
requireResolversToMatchSchema !== 'ignore'
110111
) {
111112
throw new Error(`${type.name}.${fieldName} was defined in resolvers, but not present within ${type.name}`);
112113
}
113114
});
114115
} else if (isUnionType(type)) {
115116
Object.keys(resolverValue).forEach(fieldName => {
116-
if (!fieldName.startsWith('__') && !allowResolversNotInSchema) {
117+
if (
118+
!fieldName.startsWith('__') &&
119+
requireResolversToMatchSchema &&
120+
requireResolversToMatchSchema !== 'ignore'
121+
) {
117122
throw new Error(
118123
`${type.name}.${fieldName} was defined in resolvers, but ${type.name} is not an object or interface type`
119124
);
@@ -125,7 +130,7 @@ export function addResolversToSchema(
125130
const fields = type.getFields();
126131
const field = fields[fieldName];
127132

128-
if (field == null && !allowResolversNotInSchema) {
133+
if (field == null && requireResolversToMatchSchema && requireResolversToMatchSchema !== 'ignore') {
129134
throw new Error(`${typeName}.${fieldName} defined in resolvers, but not in schema`);
130135
}
131136

packages/schema/src/assertResolversPresent.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { GraphQLSchema, GraphQLField, getNamedType, isScalarType } from 'graphql';
22

3-
import { IResolverValidationOptions, forEachField } from '@graphql-tools/utils';
3+
import { IResolverValidationOptions, forEachField, ValidatorBehavior } from '@graphql-tools/utils';
44

55
export function assertResolversPresent(
66
schema: GraphQLSchema,
77
resolverValidationOptions: IResolverValidationOptions = {}
88
): void {
99
const {
10-
requireResolversForArgs = false,
11-
requireResolversForNonScalar = false,
12-
requireResolversForAllFields = false,
10+
requireResolversForArgs,
11+
requireResolversForNonScalar,
12+
requireResolversForAllFields,
1313
} = resolverValidationOptions;
1414

1515
if (requireResolversForAllFields && (requireResolversForArgs || requireResolversForNonScalar)) {
@@ -23,32 +23,44 @@ export function assertResolversPresent(
2323
forEachField(schema, (field, typeName, fieldName) => {
2424
// requires a resolver for *every* field.
2525
if (requireResolversForAllFields) {
26-
expectResolver(field, typeName, fieldName);
26+
expectResolver('requireResolversForAllFields', requireResolversForAllFields, field, typeName, fieldName);
2727
}
2828

2929
// requires a resolver on every field that has arguments
3030
if (requireResolversForArgs && field.args.length > 0) {
31-
expectResolver(field, typeName, fieldName);
31+
expectResolver('requireResolversForArgs', requireResolversForArgs, field, typeName, fieldName);
3232
}
3333

3434
// requires a resolver on every field that returns a non-scalar type
35-
if (requireResolversForNonScalar && !isScalarType(getNamedType(field.type))) {
36-
expectResolver(field, typeName, fieldName);
35+
if (requireResolversForNonScalar !== 'ignore' && !isScalarType(getNamedType(field.type))) {
36+
expectResolver('requireResolversForNonScalar', requireResolversForNonScalar, field, typeName, fieldName);
3737
}
3838
});
3939
}
4040

41-
function expectResolver(field: GraphQLField<any, any>, typeName: string, fieldName: string) {
41+
function expectResolver(
42+
validator: string,
43+
behavior: ValidatorBehavior,
44+
field: GraphQLField<any, any>,
45+
typeName: string,
46+
fieldName: string
47+
) {
4248
if (!field.resolve) {
43-
// eslint-disable-next-line no-console
44-
console.warn(
45-
`Resolver missing for "${typeName}.${fieldName}".
46-
To disable this warning check pass;
47-
resolverValidationOptions: {
48-
requireResolversForNonScalar: false
49-
}
50-
`
51-
);
49+
const message = `Resolver missing for "${typeName}.${fieldName}".
50+
To disable this validator, use:
51+
resolverValidationOptions: {
52+
${validator}: 'ignore'
53+
}`;
54+
55+
if (behavior === 'error') {
56+
throw new Error(message);
57+
}
58+
59+
if (behavior === 'warn') {
60+
// eslint-disable-next-line no-console
61+
console.warn(message);
62+
}
63+
5264
return;
5365
}
5466
if (typeof field.resolve !== 'function') {

packages/schema/src/checkForResolveTypeResolver.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { GraphQLSchema } from 'graphql';
22

3-
import { MapperKind, mapSchema } from '@graphql-tools/utils';
3+
import { MapperKind, mapSchema, ValidatorBehavior } from '@graphql-tools/utils';
44

55
// If we have any union or interface types throw if no there is no resolveType resolver
6-
export function checkForResolveTypeResolver(schema: GraphQLSchema, requireResolversForResolveType?: boolean) {
6+
export function checkForResolveTypeResolver(schema: GraphQLSchema, requireResolversForResolveType?: ValidatorBehavior) {
77
mapSchema(schema, {
88
[MapperKind.ABSTRACT_TYPE]: type => {
99
if (!type.resolveType && requireResolversForResolveType) {
10-
throw new Error(
11-
`Type "${type.name}" is missing a "__resolveType" resolver. Pass false into ` +
12-
'"resolverValidationOptions.requireResolversForResolveType" to disable this error.'
13-
);
10+
const message =
11+
`Type "${type.name}" is missing a "__resolveType" resolver. Pass 'ignore' into ` +
12+
'"resolverValidationOptions.requireResolversForResolveType" to disable this error.';
13+
if (requireResolversForResolveType === 'error') {
14+
throw new Error(message);
15+
}
16+
17+
if (requireResolversForResolveType === 'warn') {
18+
// eslint-disable-next-line no-console
19+
console.warn(message);
20+
}
1421
}
1522
return undefined;
1623
},

0 commit comments

Comments
 (0)