From 4fb0d80f6621587e2e3a83e6be2a5efd566a3c08 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 25 Nov 2024 21:51:33 +0200 Subject: [PATCH 1/2] add test for mixture of default and custom root types --- src/utilities/__tests__/buildASTSchema-test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 5a794a1203..3d54626f9e 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -1033,6 +1033,22 @@ describe('Schema Builder', () => { expect(schema.getSubscriptionType()).to.include({ name: 'Subscription' }); }); + it('Mixture of default and custom root operation types', () => { + const schema = buildSchema(` + extend schema { + query: SomeQuery + } + type SomeQuery + type Query + type Mutation + type Subscription + `); + + expect(schema.getQueryType()).to.include({ name: 'SomeQuery' }); + expect(schema.getMutationType()).to.include({ name: 'Mutation' }); + expect(schema.getSubscriptionType()).to.include({ name: 'Subscription' }); + }); + it('can build invalid schema', () => { // Invalid schema, because it is missing query root type const schema = buildSchema('type Mutation'); From d4303242987b5cde3693d7bd8c03b49320b583e9 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 25 Nov 2024 21:52:34 +0200 Subject: [PATCH 2/2] add fix for mixture of default and custom root type names --- src/utilities/buildASTSchema.ts | 45 ++--------------------- src/utilities/extendSchema.ts | 64 ++++++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index 5be0b6e421..355feae0cd 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -3,7 +3,6 @@ import type { ParseOptions } from '../language/parser.js'; import { parse } from '../language/parser.js'; import type { Source } from '../language/source.js'; -import { specifiedDirectives } from '../type/directives.js'; import type { GraphQLSchemaValidationOptions } from '../type/schema.js'; import { GraphQLSchema } from '../type/schema.js'; @@ -38,49 +37,9 @@ export function buildASTSchema( assertValidSDL(documentAST); } - const emptySchemaConfig = { - description: undefined, - types: [], - directives: [], - extensions: Object.create(null), - extensionASTNodes: [], - assumeValid: false, - }; - const config = extendSchemaImpl(emptySchemaConfig, documentAST, options); + const config = extendSchemaImpl(documentAST, undefined, options); - if (config.astNode == null) { - for (const type of config.types) { - switch (type.name) { - // Note: While this could make early assertions to get the correctly - // typed values below, that would throw immediately while type system - // validation with validateSchema() will produce more actionable results. - case 'Query': - // @ts-expect-error validated in `validateSchema` - config.query = type; - break; - case 'Mutation': - // @ts-expect-error validated in `validateSchema` - config.mutation = type; - break; - case 'Subscription': - // @ts-expect-error validated in `validateSchema` - config.subscription = type; - break; - } - } - } - - const directives = [ - ...config.directives, - // If specified directives were not explicitly declared, add them. - ...specifiedDirectives.filter((stdDirective) => - config.directives.every( - (directive) => directive.name !== stdDirective.name, - ), - ), - ]; - - return new GraphQLSchema({ ...config, directives }); + return new GraphQLSchema(config); } /** diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 36e357b2ba..9707cc6057 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -64,6 +64,7 @@ import { GraphQLOneOfDirective, GraphQLSpecifiedByDirective, isSpecifiedDirective, + specifiedDirectives, } from '../type/directives.js'; import { introspectionTypes, @@ -116,7 +117,7 @@ export function extendSchema( } const schemaConfig = schema.toConfig(); - const extendedConfig = extendSchemaImpl(schemaConfig, documentAST, options); + const extendedConfig = extendSchemaImpl(documentAST, schemaConfig, options); return schemaConfig === extendedConfig ? schema : new GraphQLSchema(extendedConfig); @@ -126,10 +127,19 @@ export function extendSchema( * @internal */ export function extendSchemaImpl( - schemaConfig: GraphQLSchemaNormalizedConfig, documentAST: DocumentNode, + originalSchemaConfig: GraphQLSchemaNormalizedConfig | undefined, options?: Options, ): GraphQLSchemaNormalizedConfig { + const schemaConfig: GraphQLSchemaNormalizedConfig = originalSchemaConfig ?? { + description: undefined, + types: [], + directives: [...specifiedDirectives], + extensions: Object.create(null), + extensionASTNodes: [], + assumeValid: false, + }; + // Collect the type definitions and extensions found in the document. const typeDefs: Array = []; @@ -211,7 +221,12 @@ export function extendSchemaImpl( // If this document contains no new types, extensions, or directives then // return the same unmodified GraphQLSchema instance. if (!isSchemaChanged) { - return schemaConfig; + return originalSchemaConfig + ? originalSchemaConfig + : { + ...schemaConfig, + directives: [...specifiedDirectives], + }; } const typeMap = new Map( @@ -230,19 +245,31 @@ export function extendSchemaImpl( subscription: schemaConfig.subscription && replaceNamedType(schemaConfig.subscription), // Then, incorporate schema definition and all schema extensions. - ...(schemaDef && getOperationTypes([schemaDef])), + ...(schemaDef + ? getOperationTypes([schemaDef]) + : !originalSchemaConfig && getDefaultOperationTypes()), ...getOperationTypes(schemaExtensions), }; + const newDirectives = directiveDefs.map(buildDirective); + const directives = originalSchemaConfig + ? [...schemaConfig.directives.map(replaceDirective), ...newDirectives] + : [ + ...newDirectives, + // If specified directives were not explicitly declared, add them. + ...specifiedDirectives.filter((stdDirective) => + newDirectives.every( + (directive) => directive.name !== stdDirective.name, + ), + ), + ]; + // Then produce and return a Schema config with these types. return { description: schemaDef?.description?.value ?? schemaConfig.description, ...operationTypes, types: Array.from(typeMap.values()), - directives: [ - ...schemaConfig.directives.map(replaceDirective), - ...directiveDefs.map(buildDirective), - ], + directives, extensions: schemaConfig.extensions, astNode: schemaDef ?? schemaConfig.astNode, extensionASTNodes: schemaConfig.extensionASTNodes.concat(schemaExtensions), @@ -431,6 +458,27 @@ export function extendSchemaImpl( }; } + function getDefaultOperationTypes(): { + query?: Maybe; + mutation?: Maybe; + subscription?: Maybe; + } { + const opTypes = {}; + for (const typeName of ['Query', 'Mutation', 'Subscription']) { + const operationType = typeMap.get(typeName); + + if (operationType) { + // Note: While this could make early assertions to get the correctly + // typed values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. + // @ts-expect-error + opTypes[typeName.toLowerCase()] = operationType; + } + } + + return opTypes; + } + function getOperationTypes( nodes: ReadonlyArray, ): {