Skip to content
Closed
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
37 changes: 37 additions & 0 deletions .changeset/large-tigers-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
'@graphql-tools/stitch': patch
---

Validation of required selection sets in the additional resolvers and type merging configuration

```ts
stitchSchemas({
resolvers: {
Book: {
title: {
// This resolver will throw an error if the selection set does not contain the `nonExistingFieldInBook` field
// Stitching validates the selection set of the field resolver
selectionSet: '{ nonExistingFieldInBook }',
resolve() {

}
}
}
},
merge: {
Book: {
// This configuration will throw an error if the selection set does not contain the `nonExistingFieldInBook` field
// Stitching validates the selection set of the type merging configuration
selectionSet: '{ nonExistingFieldInBook }',
fields: {
title: {
// This configuration will throw an error if the selection set does not contain the `nonExistingFieldInBook` field
// Stitching validates the selection set of the field configuration
selectionSet: '{ nonExistingFieldInBook }',
fieldName: 'title'
}
}
}
}
})
```
20 changes: 19 additions & 1 deletion packages/stitch/src/stitchSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,12 @@ export function stitchSchemas<
mergeDirectives,
});

const errors: Error[] = [];

let stitchingInfo = createStitchingInfo(
subschemaMap,
typeCandidates,
errors,
mergeTypes,
);

Expand Down Expand Up @@ -131,7 +134,22 @@ export function stitchSchemas<
? extendResolversFromInterfaces(schema, resolverMap)
: resolverMap;

stitchingInfo = completeStitchingInfo(stitchingInfo, finalResolvers, schema);
stitchingInfo = completeStitchingInfo(
stitchingInfo,
finalResolvers,
schema,
errors,
);

if (errors.length > 0) {
if (errors.length === 1) {
throw errors[0];
}
throw new AggregateError(
errors,
`Encountered ${errors.length} errors while validating stitching configuration;\n${errors.map((err) => err.message)}`,
);
}

schema = addResolversToSchema({
schema,
Expand Down
77 changes: 72 additions & 5 deletions packages/stitch/src/stitchingInfo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
getTypeInfoWithType,
MergedTypeInfo,
MergedTypeResolver,
StitchingInfo,
Expand Down Expand Up @@ -29,6 +30,8 @@ import {
Kind,
print,
SelectionSetNode,
visit,
visitWithTypeInfo,
} from 'graphql';
import { createDelegationPlanBuilder } from './createDelegationPlanBuilder.js';
import { createMergedTypeResolver } from './createMergedTypeResolver.js';
Expand All @@ -42,9 +45,10 @@ export function createStitchingInfo<
Subschema<any, any, any, TContext>
>,
typeCandidates: Record<string, Array<MergeTypeCandidate<TContext>>>,
errors: Error[],
mergeTypes?: boolean | Array<string> | MergeTypeFilter<TContext>,
): StitchingInfo<TContext> {
const mergedTypes = createMergedTypes(typeCandidates, mergeTypes);
const mergedTypes = createMergedTypes(typeCandidates, errors, mergeTypes);

return {
subschemaMap,
Expand All @@ -59,6 +63,7 @@ function createMergedTypes<
TContext extends Record<string, any> = Record<string, any>,
>(
typeCandidates: Record<string, Array<MergeTypeCandidate<TContext>>>,
errors: Error[],
mergeTypes?: boolean | Array<string> | MergeTypeFilter<TContext>,
): Record<string, MergedTypeInfo<TContext>> {
const mergedTypes: Record<string, MergedTypeInfo<TContext>> = Object.create(
Expand Down Expand Up @@ -169,6 +174,26 @@ function createMergedTypes<
noLocation: true,
},
);
const typeInfo = getTypeInfoWithType(
subschema.transformedSchema,
subschema.transformedSchema.getType(typeName),
);
visit(
selectionSet,
visitWithTypeInfo(typeInfo, {
[Kind.FIELD](node: FieldNode) {
if (!typeInfo.getFieldDef()) {
errors.push(
new Error(
`In the type merging configuration "${typeName}", the required selection set ${mergedTypeConfig.selectionSet} does not conform to the schema.\n` +
`The field "${node.name.value}" doesn't exist on the type "${typeName}".\n` +
`Make sure the fields inside the "selectionSet" are valid fields of the type "${typeName}".`,
),
);
}
},
}),
);
selectionSets.set(subschema, selectionSet);
}

Expand All @@ -178,11 +203,35 @@ function createMergedTypes<
if (mergedTypeConfig.fields[fieldName]?.selectionSet) {
const rawFieldSelectionSet =
mergedTypeConfig.fields[fieldName].selectionSet;
parsedFieldSelectionSets[fieldName] = rawFieldSelectionSet
? parseSelectionSet(rawFieldSelectionSet, {
if (rawFieldSelectionSet) {
const parsedSelectionSet = parseSelectionSet(
rawFieldSelectionSet,
{
noLocation: true,
})
: undefined;
},
);
const typeInfo = getTypeInfoWithType(
subschema.transformedSchema,
subschema.transformedSchema.getType(typeName),
);
visit(
parsedSelectionSet,
visitWithTypeInfo(typeInfo, {
[Kind.FIELD](node: FieldNode) {
if (!typeInfo.getFieldDef()) {
errors.push(
new Error(
`In the type merging configuration for "${typeName}.${fieldName}", the required selection set ${rawFieldSelectionSet} does not conform to the schema.\n` +
`The field "${node.name.value}" doesn't exist on the type "${typeName}".\n` +
`Make sure the fields inside the "selectionSet" are valid fields of the type "${typeName}".`,
),
);
}
},
}),
);
parsedFieldSelectionSets[fieldName] = parsedSelectionSet;
}
}
if (mergedTypeConfig.fields[fieldName]?.provides) {
let providedSelectionsForSubschema =
Expand Down Expand Up @@ -324,6 +373,7 @@ export function completeStitchingInfo<TContext = Record<string, any>>(
stitchingInfo: StitchingInfo<TContext>,
resolvers: IResolvers,
schema: GraphQLSchema,
errors: Error[],
): StitchingInfo<TContext> {
const {
fieldNodesByType,
Expand Down Expand Up @@ -437,6 +487,23 @@ export function completeStitchingInfo<TContext = Record<string, any>>(
const selectionSet = parseSelectionSet(field.selectionSet, {
noLocation: true,
});
const typeInfo = getTypeInfoWithType(schema, type);
visit(
selectionSet,
visitWithTypeInfo(typeInfo, {
[Kind.FIELD](node: FieldNode) {
if (!typeInfo.getFieldDef()) {
errors.push(
new Error(
`In the additional resolver "${typeName}.${fieldName}", the required selection set "${field.selectionSet}" does not conform to the schema.\n` +
`The field "${node.name.value}" doesn't exist on the type "${typeName}".\n` +
`Make sure the fields inside the "selectionSet" are valid fields of the type "${typeName}".`,
),
);
}
},
}),
);
updateSelectionSetMap(
selectionSetsByField,
typeName,
Expand Down
Loading