Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(graphql): Throw an error when multiple resolvers define the same field #2423

Merged
merged 4 commits into from Sep 30, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -46,4 +46,9 @@ export interface BuildSchemaOptions {
* Array of global field middleware functions
*/
fieldMiddleware?: FieldMiddleware[];

/**
* Set to true if it should throw an error when the same Query / Mutation field is defined more than once
*/
noDuplicatedFields?: boolean;
}
@@ -0,0 +1,7 @@
export class MultipleFieldsWithSameNameError extends Error {
constructor(field: string, objectTypeName: string) {
super(
`Cannot define multiple fields with the same name "${field}" for type "${objectTypeName}"`,
);
}
}
Expand Up @@ -6,6 +6,7 @@ import { OrphanedReferenceRegistry } from '../services/orphaned-reference.regist
import { ArgsFactory } from './args.factory';
import { AstDefinitionNodeFactory } from './ast-definition-node.factory';
import { OutputTypeFactory } from './output-type.factory';
import { MultipleFieldsWithSameNameError } from '../errors/multiple-fields-with-same-name.error';

export type FieldsFactory<T = any, U = any> = (
handlers: ResolverTypeMetadata[],
Expand All @@ -27,7 +28,7 @@ export class RootTypeFactory {
objectTypeName: 'Subscription' | 'Mutation' | 'Query',
options: BuildSchemaOptions,
fieldsFactory: FieldsFactory = (handlers) =>
this.generateFields(handlers, options),
this.generateFields(handlers, options, objectTypeName),
): GraphQLObjectType {
const handlers = typeRefs
? resolversMetadata.filter((query) => typeRefs.includes(query.target))
Expand All @@ -45,6 +46,7 @@ export class RootTypeFactory {
generateFields<T = any, U = any>(
handlers: ResolverTypeMetadata[],
options: BuildSchemaOptions,
objectTypeName: string,
): GraphQLFieldConfigMap<T, U> {
const fieldConfigMap: GraphQLFieldConfigMap<T, U> = {};

Expand All @@ -66,6 +68,11 @@ export class RootTypeFactory {
);

const key = handler.schemaName;

if (fieldConfigMap[key] && options.noDuplicatedFields) {
throw new MultipleFieldsWithSameNameError(key, objectTypeName);
}

fieldConfigMap[key] = {
type,
args: this.argsFactory.create(handler.methodArgs, options),
Expand Down
@@ -0,0 +1,126 @@
import { Test } from '@nestjs/testing';
import {
GraphQLSchemaBuilderModule,
Query,
Mutation,
Resolver,
TypeMetadataStorage,
} from '../../../lib';
import { RootTypeFactory } from '../../../lib/schema-builder/factories/root-type.factory';
import { LazyMetadataStorage } from '../../../lib/schema-builder/storages/lazy-metadata.storage';
import { MultipleFieldsWithSameNameError } from '../../../lib/schema-builder/errors/multiple-fields-with-same-name.error';
import { ResolverTypeMetadata } from '../../../lib/schema-builder/metadata';

describe('RootTypeFactory', () => {
let rootTypeFactory: RootTypeFactory;

beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [GraphQLSchemaBuilderModule],
}).compile();

rootTypeFactory = module.get(RootTypeFactory);
});

afterEach(() => {
TypeMetadataStorage.clear();
});

describe('generateFields', () => {
describe.each([
['queries', Query, 'Query', TypeMetadataStorage.getQueriesMetadata],
[
'mutations',
Mutation,
'Mutation',
TypeMetadataStorage.getMutationsMetadata,
],
])(
'when duplicate %s are defined',
(_, Decorator, objectTypeName, metadataFn) => {
let metadata: ResolverTypeMetadata[];

beforeEach(() => {
LazyMetadataStorage.load([
booleanResolverFactory(Decorator),
booleanResolverFactory(Decorator),
]);
TypeMetadataStorage.compile();

metadata = metadataFn.apply(TypeMetadataStorage);
});

it('should throw an error with noDuplicateFields: true', () => {
expect(() =>
rootTypeFactory.generateFields(
metadata,
{ noDuplicatedFields: true },
objectTypeName,
),
).toThrow(MultipleFieldsWithSameNameError);
});

it('should create GraphQL fields with noDuplicateFields: false', () => {
const fields = rootTypeFactory.generateFields(
metadata,
{ noDuplicatedFields: false },
objectTypeName,
);
expect(fields).toHaveProperty('bool');
});

it('should create GraphQL fields with noDuplicateFields undefined', () => {
const fields = rootTypeFactory.generateFields(
metadata,
{},
objectTypeName,
);
expect(fields).toHaveProperty('bool');
});
},
);

describe('when no duplicate queries are found', () => {
beforeEach(() => {
LazyMetadataStorage.load([
booleanResolverFactory(Query),
StringResolver,
]);
TypeMetadataStorage.compile();
});

it('should correctly create GraphQL fields', () => {
const queriesMetadata = TypeMetadataStorage.getQueriesMetadata();

const fields = rootTypeFactory.generateFields(
queriesMetadata,
{ noDuplicatedFields: true },
'Query',
);

expect(fields).toHaveProperty('bool');
expect(fields).toHaveProperty('str');
});
});
});
});

function booleanResolverFactory(Decorator: typeof Query | typeof Mutation) {
@Resolver()
class BooleanResolver {
@Decorator(() => Boolean)
bool() {
return true;
}
}

return BooleanResolver;
}

@Resolver()
class StringResolver {
@Query(() => String)
str() {
return '';
}
}