Skip to content

Commit 5081422

Browse files
authored
fix(schema): prevent issues with subsequent schema builds (#1698)
* Add test to reproduce the issue * Prevent subsequent build of metadata storage * Add test case for generic resolver and field resolvers * Make a local copy of metadata storage to build it * Revert "Prevent subsequent build of metadata storage" This reverts commit cc94f97. * Add changelog entry
1 parent 1336d84 commit 5081422

File tree

4 files changed

+176
-21
lines changed

4 files changed

+176
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
- make possible creating custom decorators on resolver class level - `createResolverClassMiddlewareDecorator`
1212
- support registering custom arg decorator via `createParameterDecorator` and its second argument `CustomParameterOptions` - `arg` (#1325)
1313

14+
## Fixes
15+
16+
- properly build multiple schemas with generic resolvers, args and field resolvers (#1321)
17+
1418
### Others
1519

1620
- **Breaking Change**: update `graphql-scalars` peer dependency to `^1.23.0`

src/schema/schema-generator.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
import { type InterfaceClassMetadata } from "@/metadata/definitions/interface-class-metadata";
4141
import { type ObjectClassMetadata } from "@/metadata/definitions/object-class-metadata";
4242
import { getMetadataStorage } from "@/metadata/getMetadataStorage";
43+
import { MetadataStorage } from "@/metadata/metadata-storage";
4344
import {
4445
createAdvancedFieldResolver,
4546
createBasicFieldResolver,
@@ -118,10 +119,15 @@ export abstract class SchemaGenerator {
118119

119120
private static usedInterfaceTypes = new Set<Function>();
120121

122+
private static metadataStorage: MetadataStorage;
123+
121124
static generateFromMetadata(options: SchemaGeneratorOptions): GraphQLSchema {
125+
this.metadataStorage = Object.assign(new MetadataStorage(), getMetadataStorage());
126+
this.metadataStorage.build(options);
127+
122128
this.checkForErrors(options);
123129
BuildContext.create(options);
124-
getMetadataStorage().build(options);
130+
125131
this.buildTypesInfo(options.resolvers);
126132

127133
const orphanedTypes = options.orphanedTypes ?? [];
@@ -152,7 +158,7 @@ export abstract class SchemaGenerator {
152158

153159
private static checkForErrors(options: SchemaGeneratorOptions) {
154160
ensureInstalledCorrectGraphQLPackage();
155-
if (getMetadataStorage().authorizedFields.length !== 0 && options.authChecker === undefined) {
161+
if (this.metadataStorage.authorizedFields.length !== 0 && options.authChecker === undefined) {
156162
throw new Error(
157163
"You need to provide `authChecker` function for `@Authorized` decorator usage!",
158164
);
@@ -189,7 +195,7 @@ export abstract class SchemaGenerator {
189195
}
190196

191197
private static buildTypesInfo(resolvers: Function[]) {
192-
this.unionTypesInfo = getMetadataStorage().unions.map<UnionTypeInfo>(unionMetadata => {
198+
this.unionTypesInfo = this.metadataStorage.unions.map<UnionTypeInfo>(unionMetadata => {
193199
// use closure to capture values from this selected schema build
194200
const unionObjectTypesInfo: ObjectTypeInfo[] = [];
195201
// called once after building all `objectTypesInfo`
@@ -232,7 +238,7 @@ export abstract class SchemaGenerator {
232238
};
233239
});
234240

235-
this.enumTypesInfo = getMetadataStorage().enums.map<EnumTypeInfo>(enumMetadata => {
241+
this.enumTypesInfo = this.metadataStorage.enums.map<EnumTypeInfo>(enumMetadata => {
236242
const enumMap = getEnumValuesMap(enumMetadata.enumObj);
237243
return {
238244
enumObj: enumMetadata.enumObj,
@@ -253,7 +259,7 @@ export abstract class SchemaGenerator {
253259
};
254260
});
255261

256-
this.objectTypesInfo = getMetadataStorage().objectTypes.map<ObjectTypeInfo>(objectType => {
262+
this.objectTypesInfo = this.metadataStorage.objectTypes.map<ObjectTypeInfo>(objectType => {
257263
const objectSuperClass = Object.getPrototypeOf(objectType.target);
258264
const hasExtended = objectSuperClass.prototype !== undefined;
259265
const getSuperClassType = () => {
@@ -300,7 +306,7 @@ export abstract class SchemaGenerator {
300306
// support for implicitly implementing interfaces
301307
// get fields from interfaces definitions
302308
if (objectType.interfaceClasses) {
303-
const implementedInterfaces = getMetadataStorage().interfaceTypes.filter(it =>
309+
const implementedInterfaces = this.metadataStorage.interfaceTypes.filter(it =>
304310
objectType.interfaceClasses!.includes(it.target),
305311
);
306312
implementedInterfaces.forEach(it => {
@@ -312,7 +318,7 @@ export abstract class SchemaGenerator {
312318

313319
let fields = fieldsMetadata.reduce<GraphQLFieldConfigMap<any, any>>(
314320
(fieldsMap, field) => {
315-
const { fieldResolvers } = getMetadataStorage();
321+
const { fieldResolvers } = this.metadataStorage;
316322
const filteredFieldResolversMetadata = fieldResolvers.filter(
317323
it => it.kind === "internal" || resolvers.includes(it.target),
318324
);
@@ -369,7 +375,7 @@ export abstract class SchemaGenerator {
369375
};
370376
});
371377

372-
this.interfaceTypesInfo = getMetadataStorage().interfaceTypes.map<InterfaceTypeInfo>(
378+
this.interfaceTypesInfo = this.metadataStorage.interfaceTypes.map<InterfaceTypeInfo>(
373379
interfaceType => {
374380
const interfaceSuperClass = Object.getPrototypeOf(interfaceType.target);
375381
const hasExtended = interfaceSuperClass.prototype !== undefined;
@@ -381,8 +387,8 @@ export abstract class SchemaGenerator {
381387
};
382388

383389
// fetch ahead the subset of object types that implements this interface
384-
const implementingObjectTypesTargets = getMetadataStorage()
385-
.objectTypes.filter(
390+
const implementingObjectTypesTargets = this.metadataStorage.objectTypes
391+
.filter(
386392
objectType =>
387393
objectType.interfaceClasses &&
388394
objectType.interfaceClasses.includes(interfaceType.target),
@@ -419,7 +425,7 @@ export abstract class SchemaGenerator {
419425
// support for implicitly implementing interfaces
420426
// get fields from interfaces definitions
421427
if (interfaceType.interfaceClasses) {
422-
const implementedInterfacesMetadata = getMetadataStorage().interfaceTypes.filter(
428+
const implementedInterfacesMetadata = this.metadataStorage.interfaceTypes.filter(
423429
it => interfaceType.interfaceClasses!.includes(it.target),
424430
);
425431
implementedInterfacesMetadata.forEach(it => {
@@ -431,7 +437,7 @@ export abstract class SchemaGenerator {
431437

432438
let fields = fieldsMetadata!.reduce<GraphQLFieldConfigMap<any, any>>(
433439
(fieldsMap, field) => {
434-
const fieldResolverMetadata = getMetadataStorage().fieldResolvers.find(
440+
const fieldResolverMetadata = this.metadataStorage.fieldResolvers.find(
435441
resolver =>
436442
resolver.getObjectType!() === field.target &&
437443
resolver.methodName === field.name,
@@ -490,7 +496,7 @@ export abstract class SchemaGenerator {
490496
},
491497
);
492498

493-
this.inputTypesInfo = getMetadataStorage().inputTypes.map<InputObjectTypeInfo>(inputType => {
499+
this.inputTypesInfo = this.metadataStorage.inputTypes.map<InputObjectTypeInfo>(inputType => {
494500
const objectSuperClass = Object.getPrototypeOf(inputType.target);
495501
const getSuperClassType = () => {
496502
const superClassTypeInfo = this.inputTypesInfo.find(
@@ -549,7 +555,7 @@ export abstract class SchemaGenerator {
549555
}
550556

551557
private static buildRootQueryType(resolvers: Function[]): GraphQLObjectType {
552-
const queriesHandlers = this.filterHandlersByResolvers(getMetadataStorage().queries, resolvers);
558+
const queriesHandlers = this.filterHandlersByResolvers(this.metadataStorage.queries, resolvers);
553559

554560
return new GraphQLObjectType({
555561
name: "Query",
@@ -559,7 +565,7 @@ export abstract class SchemaGenerator {
559565

560566
private static buildRootMutationType(resolvers: Function[]): GraphQLObjectType | undefined {
561567
const mutationsHandlers = this.filterHandlersByResolvers(
562-
getMetadataStorage().mutations,
568+
this.metadataStorage.mutations,
563569
resolvers,
564570
);
565571
if (mutationsHandlers.length === 0) {
@@ -574,7 +580,7 @@ export abstract class SchemaGenerator {
574580

575581
private static buildRootSubscriptionType(resolvers: Function[]): GraphQLObjectType | undefined {
576582
const subscriptionsHandlers = this.filterHandlersByResolvers(
577-
getMetadataStorage().subscriptions,
583+
this.metadataStorage.subscriptions,
578584
resolvers,
579585
);
580586
if (subscriptionsHandlers.length === 0) {
@@ -735,8 +741,8 @@ export abstract class SchemaGenerator {
735741
input.index,
736742
input.name,
737743
);
738-
const argDirectives = getMetadataStorage()
739-
.argumentDirectives.filter(
744+
const argDirectives = this.metadataStorage.argumentDirectives
745+
.filter(
740746
it =>
741747
it.target === target &&
742748
it.fieldName === propertyName &&
@@ -752,7 +758,7 @@ export abstract class SchemaGenerator {
752758
astNode: getInputValueDefinitionNode(input.name, type, argDirectives),
753759
};
754760
} else if (param.kind === "args") {
755-
const argumentType = getMetadataStorage().argumentTypes.find(
761+
const argumentType = this.metadataStorage.argumentTypes.find(
756762
it => it.target === param.getType(),
757763
);
758764
if (!argumentType) {
@@ -771,7 +777,7 @@ export abstract class SchemaGenerator {
771777
inheritanceChainClasses.push(superClass);
772778
}
773779
for (const argsTypeClass of inheritanceChainClasses.reverse()) {
774-
const inheritedArgumentType = getMetadataStorage().argumentTypes.find(
780+
const inheritedArgumentType = this.metadataStorage.argumentTypes.find(
775781
it => it.target === argsTypeClass,
776782
);
777783
if (inheritedArgumentType) {

tests/functional/generic-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ describe("Generic types", () => {
408408
let schema: GraphQLSchema;
409409
let schemaIntrospection: IntrospectionSchema;
410410

411-
beforeAll(async () => {
411+
beforeEach(async () => {
412412
function Base<TType extends object>(TTypeClass: ClassType<TType>) {
413413
@ObjectType()
414414
class BaseClass {

tests/functional/resolvers.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2582,4 +2582,149 @@ describe("Resolvers", () => {
25822582
expect(dynamicField2).toBeDefined();
25832583
});
25842584
});
2585+
2586+
describe("Shared generic resolver", () => {
2587+
beforeEach(async () => {
2588+
getMetadataStorage().clear();
2589+
});
2590+
2591+
it("should handle arguments correctly on multiple buildSchema runs", async () => {
2592+
@ObjectType()
2593+
class TestResponse {
2594+
@Field()
2595+
data!: string;
2596+
}
2597+
2598+
@ArgsType()
2599+
class TestArgs {
2600+
@Field(() => Int, { defaultValue: 0 })
2601+
testField!: number;
2602+
}
2603+
2604+
function makeResolverClass() {
2605+
@Resolver(() => TestResponse)
2606+
abstract class TestResolver {
2607+
@Query(() => TestResponse)
2608+
async exampleQuery(@Args() args: TestArgs): Promise<TestResponse> {
2609+
return {
2610+
data: `resolver ${args.testField}`,
2611+
};
2612+
}
2613+
}
2614+
2615+
return TestResolver;
2616+
}
2617+
2618+
@Resolver()
2619+
class TestResolver extends makeResolverClass() {}
2620+
2621+
const fistSchemaInfo = await getSchemaInfo({
2622+
resolvers: [TestResolver],
2623+
});
2624+
2625+
expect(fistSchemaInfo.queryType.fields).toHaveLength(1);
2626+
expect(fistSchemaInfo.queryType.fields[0].args).toHaveLength(1);
2627+
2628+
const secondSchemaInfo = await getSchemaInfo({
2629+
resolvers: [TestResolver],
2630+
});
2631+
2632+
expect(secondSchemaInfo.queryType.fields).toHaveLength(1);
2633+
expect(secondSchemaInfo.queryType.fields[0].args).toHaveLength(1);
2634+
});
2635+
2636+
it("should handle field resolvers correctly on multiple buildSchema runs", async () => {
2637+
@ObjectType()
2638+
class TestResponse {
2639+
@Field()
2640+
data!: string;
2641+
}
2642+
2643+
@ArgsType()
2644+
class TestArgs {
2645+
@Field(() => Int, { defaultValue: 0 })
2646+
testField!: number;
2647+
}
2648+
2649+
function makeResolverClass() {
2650+
@Resolver(() => TestResponse)
2651+
abstract class TestResolver {
2652+
@Query(() => TestResponse)
2653+
async exampleQuery(@Args() args: TestArgs): Promise<TestResponse> {
2654+
return {
2655+
data: `resolver ${args.testField}`,
2656+
};
2657+
}
2658+
}
2659+
2660+
return TestResolver;
2661+
}
2662+
2663+
@Resolver(() => TestResponse)
2664+
class TestResolver extends makeResolverClass() {
2665+
@FieldResolver(() => Boolean, { nullable: false })
2666+
public async exampleFieldResolver(): Promise<boolean> {
2667+
return true;
2668+
}
2669+
}
2670+
2671+
@ObjectType()
2672+
class OtherTestResponse {
2673+
@Field()
2674+
data!: string;
2675+
}
2676+
2677+
@ArgsType()
2678+
class OtherTestArgs {
2679+
@Field(() => Int, { defaultValue: 0 })
2680+
testField!: number;
2681+
}
2682+
2683+
function makeOtherResolverClass() {
2684+
@Resolver(() => OtherTestResponse)
2685+
abstract class OtherTestResolver {
2686+
@Query(() => OtherTestResponse)
2687+
async exampleQuery(@Args() args: OtherTestArgs): Promise<OtherTestResponse> {
2688+
return {
2689+
data: `resolver ${args.testField}`,
2690+
};
2691+
}
2692+
}
2693+
2694+
return OtherTestResolver;
2695+
}
2696+
2697+
@Resolver(() => OtherTestResponse)
2698+
class OtherTestResolver extends makeOtherResolverClass() {
2699+
@FieldResolver(() => Boolean, { nullable: false })
2700+
public async exampleFieldResolver(): Promise<boolean> {
2701+
return true;
2702+
}
2703+
}
2704+
2705+
const fistSchemaInfo = await getSchemaInfo({
2706+
resolvers: [TestResolver],
2707+
});
2708+
2709+
const hasFoundFieldResolverInSchema = fistSchemaInfo.schemaIntrospection.types.some(
2710+
type =>
2711+
type.kind === "OBJECT" &&
2712+
type.name === "TestResponse" &&
2713+
type.fields?.some(field => field.name === "exampleFieldResolver"),
2714+
);
2715+
expect(hasFoundFieldResolverInSchema).toBeTruthy();
2716+
2717+
const secondSchemaInfo = await getSchemaInfo({
2718+
resolvers: [OtherTestResolver],
2719+
});
2720+
2721+
const hasFoundFieldResolverInOtherSchema = secondSchemaInfo.schemaIntrospection.types.some(
2722+
type =>
2723+
type.kind === "OBJECT" &&
2724+
type.name === "OtherTestResponse" &&
2725+
type.fields?.some(field => field.name === "exampleFieldResolver"),
2726+
);
2727+
expect(hasFoundFieldResolverInOtherSchema).toBeTruthy();
2728+
});
2729+
});
25852730
});

0 commit comments

Comments
 (0)