diff --git a/src/type/definition.ts b/src/type/definition.ts index 8d51201070..81d75f6c6e 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -738,7 +738,7 @@ export interface GraphQLScalarTypeConfig { extensionASTNodes?: Maybe>; } -interface GraphQLScalarTypeNormalizedConfig +export interface GraphQLScalarTypeNormalizedConfig extends GraphQLScalarTypeConfig { serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; @@ -914,7 +914,7 @@ export function defineArguments( function fieldsToFieldsConfig( fields: GraphQLFieldMap, -): GraphQLFieldConfigMap { +): GraphQLFieldNormalizedConfigMap { return mapValue(fields, (field) => ({ description: field.description, type: field.type, @@ -932,7 +932,7 @@ function fieldsToFieldsConfig( */ export function argsToArgsConfig( args: ReadonlyArray, -): GraphQLFieldConfigArgumentMap { +): GraphQLFieldNormalizedConfigArgumentMap { return keyValMap( args, (arg) => arg.name, @@ -959,10 +959,10 @@ export interface GraphQLObjectTypeConfig { extensionASTNodes?: Maybe>; } -interface GraphQLObjectTypeNormalizedConfig +export interface GraphQLObjectTypeNormalizedConfig extends GraphQLObjectTypeConfig { interfaces: ReadonlyArray; - fields: GraphQLFieldConfigMap; + fields: GraphQLFieldNormalizedConfigMap; extensions: Readonly>; extensionASTNodes: ReadonlyArray; } @@ -1035,8 +1035,17 @@ export interface GraphQLFieldConfig { astNode?: Maybe; } +export interface GraphQLFieldNormalizedConfig + extends GraphQLFieldConfig { + args: GraphQLFieldNormalizedConfigArgumentMap; + extensions: Readonly>; +} + export type GraphQLFieldConfigArgumentMap = ObjMap; +export type GraphQLFieldNormalizedConfigArgumentMap = + ObjMap; + /** * Custom extensions * @@ -1060,10 +1069,18 @@ export interface GraphQLArgumentConfig { astNode?: Maybe; } +export interface GraphQLArgumentNormalizedConfig extends GraphQLArgumentConfig { + extensions: Readonly; +} + export type GraphQLFieldConfigMap = ObjMap< GraphQLFieldConfig >; +export type GraphQLFieldNormalizedConfigMap = ObjMap< + GraphQLFieldNormalizedConfig +>; + export interface GraphQLField { name: string; description: Maybe; @@ -1229,10 +1246,10 @@ export interface GraphQLInterfaceTypeConfig { extensionASTNodes?: Maybe>; } -interface GraphQLInterfaceTypeNormalizedConfig +export interface GraphQLInterfaceTypeNormalizedConfig extends GraphQLInterfaceTypeConfig { interfaces: ReadonlyArray; - fields: GraphQLFieldConfigMap; + fields: GraphQLFieldNormalizedConfigMap; extensions: Readonly; extensionASTNodes: ReadonlyArray; } @@ -1348,7 +1365,7 @@ export interface GraphQLUnionTypeConfig { extensionASTNodes?: Maybe>; } -interface GraphQLUnionTypeNormalizedConfig +export interface GraphQLUnionTypeNormalizedConfig extends GraphQLUnionTypeConfig { types: ReadonlyArray; extensions: Readonly; @@ -1594,8 +1611,8 @@ export interface GraphQLEnumTypeConfig { extensionASTNodes?: Maybe>; } -interface GraphQLEnumTypeNormalizedConfig extends GraphQLEnumTypeConfig { - values: ObjMap */>; +export interface GraphQLEnumTypeNormalizedConfig extends GraphQLEnumTypeConfig { + values: GraphQLEnumValueNormalizedConfigMap; extensions: Readonly; extensionASTNodes: ReadonlyArray; } @@ -1603,6 +1620,9 @@ interface GraphQLEnumTypeNormalizedConfig extends GraphQLEnumTypeConfig { export type GraphQLEnumValueConfigMap /* */ = ObjMap */>; +export type GraphQLEnumValueNormalizedConfigMap /* */ = + ObjMap */>; + /** * Custom extensions * @@ -1624,6 +1644,11 @@ export interface GraphQLEnumValueConfig { astNode?: Maybe; } +export interface GraphQLEnumValueNormalizedConfig + extends GraphQLEnumValueConfig { + extensions: Readonly; +} + export interface GraphQLEnumValue { name: string; description: Maybe; @@ -1755,9 +1780,9 @@ export interface GraphQLInputObjectTypeConfig { isOneOf?: boolean; } -interface GraphQLInputObjectTypeNormalizedConfig +export interface GraphQLInputObjectTypeNormalizedConfig extends GraphQLInputObjectTypeConfig { - fields: GraphQLInputFieldConfigMap; + fields: GraphQLInputFieldNormalizedConfigMap; extensions: Readonly; extensionASTNodes: ReadonlyArray; } @@ -1787,6 +1812,14 @@ export interface GraphQLInputFieldConfig { export type GraphQLInputFieldConfigMap = ObjMap; +export interface GraphQLInputFieldNormalizedConfig + extends GraphQLInputFieldConfig { + extensions: Readonly; +} + +export type GraphQLInputFieldNormalizedConfigMap = + ObjMap; + export interface GraphQLInputField { name: string; description: Maybe; diff --git a/src/type/directives.ts b/src/type/directives.ts index 23faa33717..fc1ebba9eb 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -10,6 +10,7 @@ import { assertName } from './assertName.js'; import type { GraphQLArgument, GraphQLFieldConfigArgumentMap, + GraphQLFieldNormalizedConfigArgumentMap, } from './definition.js'; import { argsToArgsConfig, @@ -107,8 +108,9 @@ export interface GraphQLDirectiveConfig { astNode?: Maybe; } -interface GraphQLDirectiveNormalizedConfig extends GraphQLDirectiveConfig { - args: GraphQLFieldConfigArgumentMap; +export interface GraphQLDirectiveNormalizedConfig + extends GraphQLDirectiveConfig { + args: GraphQLFieldNormalizedConfigArgumentMap; isRepeatable: boolean; extensions: Readonly; } diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 36e357b2ba..53aaf5c780 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -1,7 +1,5 @@ import { AccumulatorMap } from '../jsutils/AccumulatorMap.js'; -import { inspect } from '../jsutils/inspect.js'; import { invariant } from '../jsutils/invariant.js'; -import { mapValue } from '../jsutils/mapValue.js'; import type { Maybe } from '../jsutils/Maybe.js'; import type { @@ -31,12 +29,10 @@ import type { import { Kind } from '../language/kinds.js'; import type { - GraphQLArgumentConfig, - GraphQLEnumValueConfigMap, - GraphQLFieldConfig, + GraphQLEnumValueNormalizedConfigMap, GraphQLFieldConfigArgumentMap, - GraphQLFieldConfigMap, - GraphQLInputFieldConfigMap, + GraphQLFieldNormalizedConfigMap, + GraphQLInputFieldNormalizedConfigMap, GraphQLNamedType, GraphQLType, } from '../type/definition.js'; @@ -49,30 +45,15 @@ import { GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, - isEnumType, - isInputObjectType, - isInterfaceType, - isListType, - isNonNullType, - isObjectType, - isScalarType, - isUnionType, } from '../type/definition.js'; import { GraphQLDeprecatedDirective, GraphQLDirective, GraphQLOneOfDirective, GraphQLSpecifiedByDirective, - isSpecifiedDirective, } from '../type/directives.js'; -import { - introspectionTypes, - isIntrospectionType, -} from '../type/introspection.js'; -import { - isSpecifiedScalarType, - specifiedScalarTypes, -} from '../type/scalars.js'; +import { introspectionTypes } from '../type/introspection.js'; +import { specifiedScalarTypes } from '../type/scalars.js'; import type { GraphQLSchemaNormalizedConfig, GraphQLSchemaValidationOptions, @@ -83,6 +64,16 @@ import { assertValidSDLExtension } from '../validation/validate.js'; import { getDirectiveValues } from '../execution/values.js'; +import type { + MappedGraphQLEnumTypeConfig, + MappedGraphQLInputObjectTypeConfig, + MappedGraphQLInterfaceTypeConfig, + MappedGraphQLObjectTypeConfig, + MappedGraphQLScalarTypeConfig, + MappedGraphQLUnionTypeConfig, +} from './mapSchemaConfig.js'; +import { mapSchemaConfig, SchemaElementKind } from './mapSchemaConfig.js'; + interface Options extends GraphQLSchemaValidationOptions { /** * Set to true to assume the SDL is valid. @@ -126,7 +117,7 @@ export function extendSchema( * @internal */ export function extendSchemaImpl( - schemaConfig: GraphQLSchemaNormalizedConfig, + originalSchemaConfig: GraphQLSchemaNormalizedConfig, documentAST: DocumentNode, options?: Options, ): GraphQLSchemaNormalizedConfig { @@ -211,481 +202,410 @@ export function extendSchemaImpl( // If this document contains no new types, extensions, or directives then // return the same unmodified GraphQLSchema instance. if (!isSchemaChanged) { - return schemaConfig; - } - - const typeMap = new Map( - schemaConfig.types.map((type) => [type.name, extendNamedType(type)]), - ); - - for (const typeNode of typeDefs) { - const name = typeNode.name.value; - typeMap.set(name, stdTypeMap.get(name) ?? buildType(typeNode)); - } - - const operationTypes = { - // Get the extended root operation types. - query: schemaConfig.query && replaceNamedType(schemaConfig.query), - mutation: schemaConfig.mutation && replaceNamedType(schemaConfig.mutation), - subscription: - schemaConfig.subscription && replaceNamedType(schemaConfig.subscription), - // Then, incorporate schema definition and all schema extensions. - ...(schemaDef && getOperationTypes([schemaDef])), - ...getOperationTypes(schemaExtensions), - }; - - // 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), - ], - extensions: schemaConfig.extensions, - astNode: schemaDef ?? schemaConfig.astNode, - extensionASTNodes: schemaConfig.extensionASTNodes.concat(schemaExtensions), - assumeValid: options?.assumeValid ?? false, - }; - - // Below are functions used for producing this schema that have closed over - // this scope and have access to the schema, cache, and newly defined types. - - function replaceType(type: T): T { - if (isListType(type)) { - // @ts-expect-error - return new GraphQLList(replaceType(type.ofType)); - } - if (isNonNullType(type)) { - // @ts-expect-error - return new GraphQLNonNull(replaceType(type.ofType)); - } - // @ts-expect-error FIXME - return replaceNamedType(type); - } - - function replaceNamedType(type: T): T { - // Note: While this could make early assertions to get the correctly - // typed values, that would throw immediately while type system - // validation with validateSchema() will produce more actionable results. - return typeMap.get(type.name) as T; - } - - function replaceDirective(directive: GraphQLDirective): GraphQLDirective { - if (isSpecifiedDirective(directive)) { - // Builtin directives are not extended. - return directive; - } - - const config = directive.toConfig(); - return new GraphQLDirective({ - ...config, - args: mapValue(config.args, extendArg), - }); - } - - function extendNamedType(type: GraphQLNamedType): GraphQLNamedType { - if (isIntrospectionType(type) || isSpecifiedScalarType(type)) { - // Builtin types are not extended. - return type; - } - if (isScalarType(type)) { - return extendScalarType(type); - } - if (isObjectType(type)) { - return extendObjectType(type); - } - if (isInterfaceType(type)) { - return extendInterfaceType(type); - } - if (isUnionType(type)) { - return extendUnionType(type); - } - if (isEnumType(type)) { - return extendEnumType(type); - } - if (isInputObjectType(type)) { - return extendInputObjectType(type); - } - /* c8 ignore next 3 */ - // Not reachable, all possible type definition nodes have been considered. - invariant(false, 'Unexpected type: ' + inspect(type)); + return originalSchemaConfig; } - function extendInputObjectType( - type: GraphQLInputObjectType, - ): GraphQLInputObjectType { - const config = type.toConfig(); - const extensions = inputObjectExtensions.get(config.name) ?? []; - - return new GraphQLInputObjectType({ - ...config, - fields: () => ({ - ...mapValue(config.fields, (field) => ({ - ...field, - type: replaceType(field.type), - })), - ...buildInputFieldMap(extensions), - }), - extensionASTNodes: config.extensionASTNodes.concat(extensions), - }); - } - - function extendEnumType(type: GraphQLEnumType): GraphQLEnumType { - const config = type.toConfig(); - const extensions = enumExtensions.get(type.name) ?? []; - - return new GraphQLEnumType({ - ...config, - values: { - ...config.values, - ...buildEnumValueMap(extensions), - }, - extensionASTNodes: config.extensionASTNodes.concat(extensions), - }); - } + return mapSchemaConfig( + originalSchemaConfig, + ({ getMappedType, getMappedNamedType, addNamedType }) => { + return { + [SchemaElementKind.SCALAR]: extendScalarType, + [SchemaElementKind.OBJECT]: extendObjectType, + [SchemaElementKind.INTERFACE]: extendInterfaceType, + [SchemaElementKind.UNION]: extendUnionType, + [SchemaElementKind.INPUT_OBJECT]: extendInputObjectType, + [SchemaElementKind.ENUM]: extendEnumType, + [SchemaElementKind.SCHEMA]: extendSchemaConfig, + }; - function extendScalarType(type: GraphQLScalarType): GraphQLScalarType { - const config = type.toConfig(); - const extensions = scalarExtensions.get(config.name) ?? []; + function extendSchemaConfig( + schemaConfig: GraphQLSchemaNormalizedConfig, + ): GraphQLSchemaNormalizedConfig { + const newTypes: Array = []; + for (const typeNode of typeDefs) { + const type = + stdTypeMap.get(typeNode.name.value) ?? buildNamedType(typeNode); + addNamedType(type); + newTypes.push(type); + } + + const operationTypes = { + // Get the extended root operation types. + query: schemaConfig.query && getMappedType(schemaConfig.query), + mutation: + schemaConfig.mutation && getMappedType(schemaConfig.mutation), + subscription: + schemaConfig.subscription && + getMappedType(schemaConfig.subscription), + // Then, incorporate schema definition and all schema extensions. + ...(schemaDef && getOperationTypes([schemaDef])), + ...getOperationTypes(schemaExtensions), + }; - let specifiedByURL = config.specifiedByURL; - for (const extensionNode of extensions) { - specifiedByURL = getSpecifiedByURL(extensionNode) ?? specifiedByURL; - } + // Then produce and return a Schema config with these types. + return { + description: + schemaDef?.description?.value ?? schemaConfig.description, + ...operationTypes, + types: [...schemaConfig.types, ...newTypes], + directives: [ + ...schemaConfig.directives, + ...directiveDefs.map(buildDirective), + ], + extensions: schemaConfig.extensions, + astNode: schemaDef ?? schemaConfig.astNode, + extensionASTNodes: + schemaConfig.extensionASTNodes.concat(schemaExtensions), + assumeValid: options?.assumeValid ?? false, + }; + } - return new GraphQLScalarType({ - ...config, - specifiedByURL, - extensionASTNodes: config.extensionASTNodes.concat(extensions), - }); - } + function extendScalarType( + config: MappedGraphQLScalarTypeConfig, + ): MappedGraphQLScalarTypeConfig { + const extensions = scalarExtensions.get(config.name) ?? []; + let specifiedByURL = config.specifiedByURL; + for (const extensionNode of extensions) { + specifiedByURL = getSpecifiedByURL(extensionNode) ?? specifiedByURL; + } + return { + ...config, + specifiedByURL, + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + } - function extendObjectType(type: GraphQLObjectType): GraphQLObjectType { - const config = type.toConfig(); - const extensions = objectExtensions.get(config.name) ?? []; - - return new GraphQLObjectType({ - ...config, - interfaces: () => [ - ...type.getInterfaces().map(replaceNamedType), - ...buildInterfaces(extensions), - ], - fields: () => ({ - ...mapValue(config.fields, extendField), - ...buildFieldMap(extensions), - }), - extensionASTNodes: config.extensionASTNodes.concat(extensions), - }); - } + function extendObjectType( + config: MappedGraphQLObjectTypeConfig, + ): MappedGraphQLObjectTypeConfig { + const extensions = objectExtensions.get(config.name) ?? []; + return { + ...config, + interfaces: () => [ + ...config.interfaces(), + ...buildInterfaces(extensions), + ], + fields: () => ({ + ...config.fields(), + ...buildFieldMap(extensions), + }), + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + } - function extendInterfaceType( - type: GraphQLInterfaceType, - ): GraphQLInterfaceType { - const config = type.toConfig(); - const extensions = interfaceExtensions.get(config.name) ?? []; - - return new GraphQLInterfaceType({ - ...config, - interfaces: () => [ - ...type.getInterfaces().map(replaceNamedType), - ...buildInterfaces(extensions), - ], - fields: () => ({ - ...mapValue(config.fields, extendField), - ...buildFieldMap(extensions), - }), - extensionASTNodes: config.extensionASTNodes.concat(extensions), - }); - } + function extendInterfaceType( + config: MappedGraphQLInterfaceTypeConfig, + ): MappedGraphQLInterfaceTypeConfig { + const extensions = interfaceExtensions.get(config.name) ?? []; + return { + ...config, + interfaces: () => [ + ...config.interfaces(), + ...buildInterfaces(extensions), + ], + fields: () => ({ + ...config.fields(), + ...buildFieldMap(extensions), + }), + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + } - function extendUnionType(type: GraphQLUnionType): GraphQLUnionType { - const config = type.toConfig(); - const extensions = unionExtensions.get(config.name) ?? []; - - return new GraphQLUnionType({ - ...config, - types: () => [ - ...type.getTypes().map(replaceNamedType), - ...buildUnionTypes(extensions), - ], - extensionASTNodes: config.extensionASTNodes.concat(extensions), - }); - } + function extendUnionType( + config: MappedGraphQLUnionTypeConfig, + ): MappedGraphQLUnionTypeConfig { + const extensions = unionExtensions.get(config.name) ?? []; + return { + ...config, + types: () => [...config.types(), ...buildUnionTypes(extensions)], + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + } - function extendField( - field: GraphQLFieldConfig, - ): GraphQLFieldConfig { - return { - ...field, - type: replaceType(field.type), - args: field.args && mapValue(field.args, extendArg), - }; - } + function extendEnumType( + config: MappedGraphQLEnumTypeConfig, + ): MappedGraphQLEnumTypeConfig { + const extensions = enumExtensions.get(config.name) ?? []; + return { + ...config, + values: () => ({ + ...config.values(), + ...buildEnumValueMap(extensions), + }), + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + } - function extendArg(arg: GraphQLArgumentConfig) { - return { - ...arg, - type: replaceType(arg.type), - }; - } + function extendInputObjectType( + config: MappedGraphQLInputObjectTypeConfig, + ): MappedGraphQLInputObjectTypeConfig { + const extensions = inputObjectExtensions.get(config.name) ?? []; + return { + ...config, + fields: () => ({ + ...config.fields(), + ...buildInputFieldMap(extensions), + }), + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + } - function getOperationTypes( - nodes: ReadonlyArray, - ): { - query?: Maybe; - mutation?: Maybe; - subscription?: Maybe; - } { - const opTypes = {}; - for (const node of nodes) { - const operationTypesNodes = node.operationTypes ?? []; - - for (const operationType of operationTypesNodes) { - // 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[operationType.operation] = getNamedType(operationType.type); + function getOperationTypes( + nodes: ReadonlyArray, + ): { + query?: Maybe; + mutation?: Maybe; + subscription?: Maybe; + } { + const opTypes = {}; + for (const node of nodes) { + const operationTypesNodes = node.operationTypes ?? []; + + for (const operationType of operationTypesNodes) { + // 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[operationType.operation] = getNamedType(operationType.type); + } + } + + return opTypes; } - } - return opTypes; - } + function getNamedType(node: NamedTypeNode): GraphQLNamedType { + const name = node.name.value; + const type = getMappedNamedType(name); + invariant(type !== undefined, `Unknown type: "${name}".`); + return type; + } - function getNamedType(node: NamedTypeNode): GraphQLNamedType { - const name = node.name.value; - const type = stdTypeMap.get(name) ?? typeMap.get(name); + function getWrappedType(node: TypeNode): GraphQLType { + if (node.kind === Kind.LIST_TYPE) { + return new GraphQLList(getWrappedType(node.type)); + } + if (node.kind === Kind.NON_NULL_TYPE) { + return new GraphQLNonNull(getWrappedType(node.type)); + } + return getNamedType(node); + } - if (type === undefined) { - throw new Error(`Unknown type: "${name}".`); - } - return type; - } + function buildDirective(node: DirectiveDefinitionNode): GraphQLDirective { + return new GraphQLDirective({ + name: node.name.value, + description: node.description?.value, + // @ts-expect-error + locations: node.locations.map(({ value }) => value), + isRepeatable: node.repeatable, + args: buildArgumentMap(node.arguments), + astNode: node, + }); + } - function getWrappedType(node: TypeNode): GraphQLType { - if (node.kind === Kind.LIST_TYPE) { - return new GraphQLList(getWrappedType(node.type)); - } - if (node.kind === Kind.NON_NULL_TYPE) { - return new GraphQLNonNull(getWrappedType(node.type)); - } - return getNamedType(node); - } + function buildFieldMap( + nodes: ReadonlyArray< + | InterfaceTypeDefinitionNode + | InterfaceTypeExtensionNode + | ObjectTypeDefinitionNode + | ObjectTypeExtensionNode + >, + ): GraphQLFieldNormalizedConfigMap { + const fieldConfigMap = Object.create(null); + for (const node of nodes) { + const nodeFields = node.fields ?? []; + + for (const field of nodeFields) { + fieldConfigMap[field.name.value] = { + // Note: While this could make assertions to get the correctly typed + // value, that would throw immediately while type system validation + // with validateSchema() will produce more actionable results. + type: getWrappedType(field.type), + description: field.description?.value, + args: buildArgumentMap(field.arguments), + deprecationReason: getDeprecationReason(field), + astNode: field, + }; + } + } + return fieldConfigMap; + } - function buildDirective(node: DirectiveDefinitionNode): GraphQLDirective { - return new GraphQLDirective({ - name: node.name.value, - description: node.description?.value, - // @ts-expect-error - locations: node.locations.map(({ value }) => value), - isRepeatable: node.repeatable, - args: buildArgumentMap(node.arguments), - astNode: node, - }); - } + function buildArgumentMap( + args: Maybe>, + ): GraphQLFieldConfigArgumentMap { + const argsNodes = args ?? []; - function buildFieldMap( - nodes: ReadonlyArray< - | InterfaceTypeDefinitionNode - | InterfaceTypeExtensionNode - | ObjectTypeDefinitionNode - | ObjectTypeExtensionNode - >, - ): GraphQLFieldConfigMap { - const fieldConfigMap = Object.create(null); - for (const node of nodes) { - const nodeFields = node.fields ?? []; - - for (const field of nodeFields) { - fieldConfigMap[field.name.value] = { + const argConfigMap = Object.create(null); + for (const arg of argsNodes) { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. - type: getWrappedType(field.type), - description: field.description?.value, - args: buildArgumentMap(field.arguments), - deprecationReason: getDeprecationReason(field), - astNode: field, - }; + const type: any = getWrappedType(arg.type); + + argConfigMap[arg.name.value] = { + type, + description: arg.description?.value, + defaultValueLiteral: arg.defaultValue, + deprecationReason: getDeprecationReason(arg), + astNode: arg, + }; + } + return argConfigMap; } - } - return fieldConfigMap; - } - function buildArgumentMap( - args: Maybe>, - ): GraphQLFieldConfigArgumentMap { - const argsNodes = args ?? []; - - const argConfigMap = Object.create(null); - for (const arg of argsNodes) { - // Note: While this could make assertions to get the correctly typed - // value, that would throw immediately while type system validation - // with validateSchema() will produce more actionable results. - const type: any = getWrappedType(arg.type); - - argConfigMap[arg.name.value] = { - type, - description: arg.description?.value, - defaultValueLiteral: arg.defaultValue, - deprecationReason: getDeprecationReason(arg), - astNode: arg, - }; - } - return argConfigMap; - } + function buildInputFieldMap( + nodes: ReadonlyArray< + InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode + >, + ): GraphQLInputFieldNormalizedConfigMap { + const inputFieldMap = Object.create(null); + for (const node of nodes) { + const fieldsNodes = node.fields ?? []; + + for (const field of fieldsNodes) { + // Note: While this could make assertions to get the correctly typed + // value, that would throw immediately while type system validation + // with validateSchema() will produce more actionable results. + const type: any = getWrappedType(field.type); + + inputFieldMap[field.name.value] = { + type, + description: field.description?.value, + defaultValueLiteral: field.defaultValue, + deprecationReason: getDeprecationReason(field), + astNode: field, + }; + } + } + return inputFieldMap; + } - function buildInputFieldMap( - nodes: ReadonlyArray< - InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode - >, - ): GraphQLInputFieldConfigMap { - const inputFieldMap = Object.create(null); - for (const node of nodes) { - const fieldsNodes = node.fields ?? []; + function buildEnumValueMap( + nodes: ReadonlyArray, + ): GraphQLEnumValueNormalizedConfigMap { + const enumValueMap = Object.create(null); + for (const node of nodes) { + const valuesNodes = node.values ?? []; + + for (const value of valuesNodes) { + enumValueMap[value.name.value] = { + description: value.description?.value, + deprecationReason: getDeprecationReason(value), + astNode: value, + }; + } + } + return enumValueMap; + } - for (const field of fieldsNodes) { + function buildInterfaces( + nodes: ReadonlyArray< + | InterfaceTypeDefinitionNode + | InterfaceTypeExtensionNode + | ObjectTypeDefinitionNode + | ObjectTypeExtensionNode + >, + ): Array { // Note: While this could make assertions to get the correctly typed - // value, that would throw immediately while type system validation - // with validateSchema() will produce more actionable results. - const type: any = getWrappedType(field.type); - - inputFieldMap[field.name.value] = { - type, - description: field.description?.value, - defaultValueLiteral: field.defaultValue, - deprecationReason: getDeprecationReason(field), - astNode: field, - }; + // values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. + // @ts-expect-error + return nodes.flatMap( + (node) => node.interfaces?.map(getNamedType) ?? [], + ); } - } - return inputFieldMap; - } - function buildEnumValueMap( - nodes: ReadonlyArray, - ): GraphQLEnumValueConfigMap { - const enumValueMap = Object.create(null); - for (const node of nodes) { - const valuesNodes = node.values ?? []; - - for (const value of valuesNodes) { - enumValueMap[value.name.value] = { - description: value.description?.value, - deprecationReason: getDeprecationReason(value), - astNode: value, - }; + function buildUnionTypes( + nodes: ReadonlyArray, + ): Array { + // Note: While this could make 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 + return nodes.flatMap((node) => node.types?.map(getNamedType) ?? []); } - } - return enumValueMap; - } - - function buildInterfaces( - nodes: ReadonlyArray< - | InterfaceTypeDefinitionNode - | InterfaceTypeExtensionNode - | ObjectTypeDefinitionNode - | ObjectTypeExtensionNode - >, - ): Array { - // Note: While this could make 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 - return nodes.flatMap((node) => node.interfaces?.map(getNamedType) ?? []); - } - function buildUnionTypes( - nodes: ReadonlyArray, - ): Array { - // Note: While this could make 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 - return nodes.flatMap((node) => node.types?.map(getNamedType) ?? []); - } - - function buildType(astNode: TypeDefinitionNode): GraphQLNamedType { - const name = astNode.name.value; - - switch (astNode.kind) { - case Kind.OBJECT_TYPE_DEFINITION: { - const extensionASTNodes = objectExtensions.get(name) ?? []; - const allNodes = [astNode, ...extensionASTNodes]; - - return new GraphQLObjectType({ - name, - description: astNode.description?.value, - interfaces: () => buildInterfaces(allNodes), - fields: () => buildFieldMap(allNodes), - astNode, - extensionASTNodes, - }); + function buildNamedType(astNode: TypeDefinitionNode): GraphQLNamedType { + const name = astNode.name.value; + + switch (astNode.kind) { + case Kind.OBJECT_TYPE_DEFINITION: { + const extensionASTNodes = objectExtensions.get(name) ?? []; + const allNodes = [astNode, ...extensionASTNodes]; + + return new GraphQLObjectType({ + name, + description: astNode.description?.value, + interfaces: () => buildInterfaces(allNodes), + fields: () => buildFieldMap(allNodes), + astNode, + extensionASTNodes, + }); + } + case Kind.INTERFACE_TYPE_DEFINITION: { + const extensionASTNodes = interfaceExtensions.get(name) ?? []; + const allNodes = [astNode, ...extensionASTNodes]; + + return new GraphQLInterfaceType({ + name, + description: astNode.description?.value, + interfaces: () => buildInterfaces(allNodes), + fields: () => buildFieldMap(allNodes), + astNode, + extensionASTNodes, + }); + } + case Kind.ENUM_TYPE_DEFINITION: { + const extensionASTNodes = enumExtensions.get(name) ?? []; + const allNodes = [astNode, ...extensionASTNodes]; + + return new GraphQLEnumType({ + name, + description: astNode.description?.value, + values: () => buildEnumValueMap(allNodes), + astNode, + extensionASTNodes, + }); + } + case Kind.UNION_TYPE_DEFINITION: { + const extensionASTNodes = unionExtensions.get(name) ?? []; + const allNodes = [astNode, ...extensionASTNodes]; + + return new GraphQLUnionType({ + name, + description: astNode.description?.value, + types: () => buildUnionTypes(allNodes), + astNode, + extensionASTNodes, + }); + } + case Kind.SCALAR_TYPE_DEFINITION: { + const extensionASTNodes = scalarExtensions.get(name) ?? []; + return new GraphQLScalarType({ + name, + description: astNode.description?.value, + specifiedByURL: getSpecifiedByURL(astNode), + astNode, + extensionASTNodes, + }); + } + case Kind.INPUT_OBJECT_TYPE_DEFINITION: { + const extensionASTNodes = inputObjectExtensions.get(name) ?? []; + const allNodes = [astNode, ...extensionASTNodes]; + + return new GraphQLInputObjectType({ + name, + description: astNode.description?.value, + fields: () => buildInputFieldMap(allNodes), + astNode, + extensionASTNodes, + isOneOf: isOneOf(astNode), + }); + } + } } - case Kind.INTERFACE_TYPE_DEFINITION: { - const extensionASTNodes = interfaceExtensions.get(name) ?? []; - const allNodes = [astNode, ...extensionASTNodes]; - - return new GraphQLInterfaceType({ - name, - description: astNode.description?.value, - interfaces: () => buildInterfaces(allNodes), - fields: () => buildFieldMap(allNodes), - astNode, - extensionASTNodes, - }); - } - case Kind.ENUM_TYPE_DEFINITION: { - const extensionASTNodes = enumExtensions.get(name) ?? []; - const allNodes = [astNode, ...extensionASTNodes]; - - return new GraphQLEnumType({ - name, - description: astNode.description?.value, - values: buildEnumValueMap(allNodes), - astNode, - extensionASTNodes, - }); - } - case Kind.UNION_TYPE_DEFINITION: { - const extensionASTNodes = unionExtensions.get(name) ?? []; - const allNodes = [astNode, ...extensionASTNodes]; - - return new GraphQLUnionType({ - name, - description: astNode.description?.value, - types: () => buildUnionTypes(allNodes), - astNode, - extensionASTNodes, - }); - } - case Kind.SCALAR_TYPE_DEFINITION: { - const extensionASTNodes = scalarExtensions.get(name) ?? []; - return new GraphQLScalarType({ - name, - description: astNode.description?.value, - specifiedByURL: getSpecifiedByURL(astNode), - astNode, - extensionASTNodes, - }); - } - case Kind.INPUT_OBJECT_TYPE_DEFINITION: { - const extensionASTNodes = inputObjectExtensions.get(name) ?? []; - const allNodes = [astNode, ...extensionASTNodes]; - - return new GraphQLInputObjectType({ - name, - description: astNode.description?.value, - fields: () => buildInputFieldMap(allNodes), - astNode, - extensionASTNodes, - isOneOf: isOneOf(astNode), - }); - } - } - } + }, + ); } const stdTypeMap = new Map( diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts index e3f25e1c4a..8f3b118c14 100644 --- a/src/utilities/lexicographicSortSchema.ts +++ b/src/utilities/lexicographicSortSchema.ts @@ -1,173 +1,110 @@ -import { inspect } from '../jsutils/inspect.js'; -import { invariant } from '../jsutils/invariant.js'; -import type { Maybe } from '../jsutils/Maybe.js'; import { naturalCompare } from '../jsutils/naturalCompare.js'; import type { ObjMap } from '../jsutils/ObjMap.js'; -import type { - GraphQLFieldConfigArgumentMap, - GraphQLFieldConfigMap, - GraphQLInputFieldConfigMap, - GraphQLNamedType, - GraphQLType, -} from '../type/definition.js'; -import { - GraphQLEnumType, - GraphQLInputObjectType, - GraphQLInterfaceType, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLUnionType, - isEnumType, - isInputObjectType, - isInterfaceType, - isListType, - isNonNullType, - isObjectType, - isScalarType, - isUnionType, -} from '../type/definition.js'; -import { GraphQLDirective } from '../type/directives.js'; -import { isIntrospectionType } from '../type/introspection.js'; +import type { GraphQLFieldNormalizedConfig } from '../type/definition.js'; +import type { GraphQLDirectiveNormalizedConfig } from '../type/directives.js'; +import type { GraphQLSchemaNormalizedConfig } from '../type/schema.js'; import { GraphQLSchema } from '../type/schema.js'; +import type { + MappedGraphQLEnumTypeConfig, + MappedGraphQLInputObjectTypeConfig, + MappedGraphQLInterfaceTypeConfig, + MappedGraphQLObjectTypeConfig, + MappedGraphQLUnionTypeConfig, +} from './mapSchemaConfig.js'; +import { mapSchemaConfig, SchemaElementKind } from './mapSchemaConfig.js'; + /** * Sort GraphQLSchema. * * This function returns a sorted copy of the given GraphQLSchema. */ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { - const schemaConfig = schema.toConfig(); - const typeMap = new Map( - sortByName(schemaConfig.types).map((type) => [ - type.name, - sortNamedType(type), - ]), + return new GraphQLSchema( + mapSchemaConfig(schema.toConfig(), () => ({ + [SchemaElementKind.OBJECT]: sortObjectOrInterfaceType, + [SchemaElementKind.FIELD_DEFINITION]: sortField, + [SchemaElementKind.INTERFACE]: sortObjectOrInterfaceType, + [SchemaElementKind.UNION]: sortUnionType, + [SchemaElementKind.ENUM]: sortEnumType, + [SchemaElementKind.INPUT_OBJECT]: sortInputObjectType, + [SchemaElementKind.DIRECTIVE_DEFINITION]: sortDirective, + [SchemaElementKind.SCHEMA]: sortSchema, + })), ); +} - return new GraphQLSchema({ - ...schemaConfig, - types: Array.from(typeMap.values()), - directives: sortByName(schemaConfig.directives).map(sortDirective), - query: replaceMaybeType(schemaConfig.query), - mutation: replaceMaybeType(schemaConfig.mutation), - subscription: replaceMaybeType(schemaConfig.subscription), - }); - - function replaceType(type: T): T { - if (isListType(type)) { - // @ts-expect-error - return new GraphQLList(replaceType(type.ofType)); - } else if (isNonNullType(type)) { - // @ts-expect-error - return new GraphQLNonNull(replaceType(type.ofType)); - } - // @ts-expect-error FIXME: TS Conversion - return replaceNamedType(type); - } - - function replaceNamedType(type: T): T { - return typeMap.get(type.name) as T; - } - - function replaceMaybeType( - maybeType: Maybe, - ): Maybe { - return maybeType && replaceNamedType(maybeType); - } +function sortObjectOrInterfaceType< + T extends MappedGraphQLObjectTypeConfig | MappedGraphQLInterfaceTypeConfig, +>(config: T): T { + return { + ...config, + interfaces: () => sortByName(config.interfaces()), + fields: () => sortObjMap(config.fields()), + }; +} - function sortDirective(directive: GraphQLDirective) { - const config = directive.toConfig(); - return new GraphQLDirective({ - ...config, - locations: sortBy(config.locations, (x) => x), - args: sortArgs(config.args), - }); - } +function sortField( + config: GraphQLFieldNormalizedConfig, +): GraphQLFieldNormalizedConfig { + return { + ...config, + args: sortObjMap(config.args), + }; +} - function sortArgs(args: GraphQLFieldConfigArgumentMap) { - return sortObjMap(args, (arg) => ({ - ...arg, - type: replaceType(arg.type), - })); - } +function sortUnionType( + config: MappedGraphQLUnionTypeConfig, +): MappedGraphQLUnionTypeConfig { + return { + ...config, + types: () => sortByName(config.types()), + }; +} - function sortFields(fieldsMap: GraphQLFieldConfigMap) { - return sortObjMap(fieldsMap, (field) => ({ - ...field, - type: replaceType(field.type), - args: field.args && sortArgs(field.args), - })); - } +function sortEnumType( + config: MappedGraphQLEnumTypeConfig, +): MappedGraphQLEnumTypeConfig { + return { + ...config, + values: () => sortObjMap(config.values()), + }; +} - function sortInputFields(fieldsMap: GraphQLInputFieldConfigMap) { - return sortObjMap(fieldsMap, (field) => ({ - ...field, - type: replaceType(field.type), - })); - } +function sortInputObjectType( + config: MappedGraphQLInputObjectTypeConfig, +): MappedGraphQLInputObjectTypeConfig { + return { + ...config, + fields: () => sortObjMap(config.fields()), + }; +} - function sortTypes( - array: ReadonlyArray, - ): Array { - return sortByName(array).map(replaceNamedType); - } +function sortDirective( + config: GraphQLDirectiveNormalizedConfig, +): GraphQLDirectiveNormalizedConfig { + return { + ...config, + locations: sortBy(config.locations, (x) => x), + args: sortObjMap(config.args), + }; +} - function sortNamedType(type: GraphQLNamedType): GraphQLNamedType { - if (isScalarType(type) || isIntrospectionType(type)) { - return type; - } - if (isObjectType(type)) { - const config = type.toConfig(); - return new GraphQLObjectType({ - ...config, - interfaces: () => sortTypes(config.interfaces), - fields: () => sortFields(config.fields), - }); - } - if (isInterfaceType(type)) { - const config = type.toConfig(); - return new GraphQLInterfaceType({ - ...config, - interfaces: () => sortTypes(config.interfaces), - fields: () => sortFields(config.fields), - }); - } - if (isUnionType(type)) { - const config = type.toConfig(); - return new GraphQLUnionType({ - ...config, - types: () => sortTypes(config.types), - }); - } - if (isEnumType(type)) { - const config = type.toConfig(); - return new GraphQLEnumType({ - ...config, - values: sortObjMap(config.values, (value) => value), - }); - } - if (isInputObjectType(type)) { - const config = type.toConfig(); - return new GraphQLInputObjectType({ - ...config, - fields: () => sortInputFields(config.fields), - }); - } - /* c8 ignore next 3 */ - // Not reachable, all possible types have been considered. - invariant(false, 'Unexpected type: ' + inspect(type)); - } +function sortSchema( + config: GraphQLSchemaNormalizedConfig, +): GraphQLSchemaNormalizedConfig { + return { + ...config, + types: sortByName(config.types), + directives: sortByName(config.directives), + }; } -function sortObjMap( - map: ObjMap, - sortValueFn: (value: T) => R, -): ObjMap { +function sortObjMap(map: ObjMap): ObjMap { const sortedMap = Object.create(null); for (const key of Object.keys(map).sort(naturalCompare)) { - sortedMap[key] = sortValueFn(map[key]); + sortedMap[key] = map[key]; } return sortedMap; } diff --git a/src/utilities/mapSchemaConfig.ts b/src/utilities/mapSchemaConfig.ts new file mode 100644 index 0000000000..fbb0990571 --- /dev/null +++ b/src/utilities/mapSchemaConfig.ts @@ -0,0 +1,504 @@ +import { inspect } from '../jsutils/inspect.js'; +import { invariant } from '../jsutils/invariant.js'; +import type { Maybe } from '../jsutils/Maybe.js'; + +import type { + GraphQLArgumentNormalizedConfig, + GraphQLEnumTypeNormalizedConfig, + GraphQLEnumValueConfig, + GraphQLEnumValueConfigMap, + GraphQLEnumValueNormalizedConfigMap, + GraphQLFieldNormalizedConfig, + GraphQLFieldNormalizedConfigArgumentMap, + GraphQLFieldNormalizedConfigMap, + GraphQLInputFieldConfig, + GraphQLInputFieldNormalizedConfigMap, + GraphQLInputObjectTypeNormalizedConfig, + GraphQLInterfaceTypeNormalizedConfig, + GraphQLNamedType, + GraphQLObjectTypeNormalizedConfig, + GraphQLScalarTypeNormalizedConfig, + GraphQLType, + GraphQLUnionTypeNormalizedConfig, +} from '../type/definition.js'; +import { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLScalarType, + GraphQLUnionType, + isEnumType, + isInputObjectType, + isInterfaceType, + isListType, + isNonNullType, + isObjectType, + isScalarType, + isUnionType, +} from '../type/definition.js'; +import type { GraphQLDirectiveNormalizedConfig } from '../type/directives.js'; +import { GraphQLDirective, isSpecifiedDirective } from '../type/directives.js'; +import { + introspectionTypes, + isIntrospectionType, +} from '../type/introspection.js'; +import { + isSpecifiedScalarType, + specifiedScalarTypes, +} from '../type/scalars.js'; +import type { GraphQLSchemaNormalizedConfig } from '../type/schema.js'; + +/** + * The set of GraphQL Schema Elements. + */ +export const SchemaElementKind = { + SCHEMA: 'SCHEMA' as const, + SCALAR: 'SCALAR' as const, + OBJECT: 'OBJECT' as const, + FIELD_DEFINITION: 'FIELD_DEFINITION' as const, + ARGUMENT_DEFINITION: 'ARGUMENT_DEFINITION' as const, + INTERFACE: 'INTERFACE' as const, + UNION: 'UNION' as const, + ENUM: 'ENUM' as const, + ENUM_VALUE: 'ENUM_VALUE' as const, + INPUT_OBJECT: 'INPUT_OBJECT' as const, + INPUT_FIELD_DEFINITION: 'INPUT_FIELD_DEFINITION' as const, + DIRECTIVE_DEFINITION: 'DIRECTIVE_DEFINITION' as const, +} as const; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type SchemaElementKind = + (typeof SchemaElementKind)[keyof typeof SchemaElementKind]; + +interface MapperHelpers { + getMappedType: (type: T) => T; + getMappedNamedType: (typeName: string) => GraphQLNamedType; + addNamedType: (type: GraphQLNamedType) => void; +} + +const stdTypeMap = new Map( + [...specifiedScalarTypes, ...introspectionTypes].map((type) => [ + type.name, + type, + ]), +); + +export type MappedGraphQLScalarTypeConfig = GraphQLScalarTypeNormalizedConfig< + any, + any +>; + +type EnsureThunks = { + [K in keyof T]: K extends ThunkFields ? () => T[K] : T[K]; +}; + +export type MappedGraphQLObjectTypeConfig = EnsureThunks< + GraphQLObjectTypeNormalizedConfig, + 'interfaces' | 'fields' +>; +export type MappedGraphQLInterfaceTypeConfig = EnsureThunks< + GraphQLInterfaceTypeNormalizedConfig, + 'interfaces' | 'fields' +>; +export type MappedGraphQLUnionTypeConfig = EnsureThunks< + GraphQLUnionTypeNormalizedConfig, + 'types' +>; +export type MappedGraphQLEnumTypeConfig = EnsureThunks< + GraphQLEnumTypeNormalizedConfig, + 'values' +>; +export type MappedGraphQLInputObjectTypeConfig = EnsureThunks< + GraphQLInputObjectTypeNormalizedConfig, + 'fields' +>; + +type ScalarTypeConfigMapper = ( + scalarConfig: MappedGraphQLScalarTypeConfig, +) => MappedGraphQLScalarTypeConfig; + +type ObjectTypeConfigMapper = ( + objectConfig: MappedGraphQLObjectTypeConfig, +) => MappedGraphQLObjectTypeConfig; + +type FieldConfigMapper = ( + fieldConfig: GraphQLFieldNormalizedConfig, + parentTypeName: string, +) => GraphQLFieldNormalizedConfig; + +type ArgumentConfigMapper = ( + argConfig: GraphQLArgumentNormalizedConfig, + fieldOrDirectiveName: string, + parentTypeName?: string | undefined, +) => GraphQLArgumentNormalizedConfig; + +type InterfaceTypeConfigMapper = ( + interfaceConfig: MappedGraphQLInterfaceTypeConfig, +) => MappedGraphQLInterfaceTypeConfig; + +type UnionTypeConfigMapper = ( + unionConfig: MappedGraphQLUnionTypeConfig, +) => MappedGraphQLUnionTypeConfig; + +type EnumTypeConfigMapper = ( + enumConfig: MappedGraphQLEnumTypeConfig, +) => MappedGraphQLEnumTypeConfig; + +type EnumValueConfigMapper = ( + enumValueConfig: GraphQLEnumValueConfig, + valueName: string, + enumName: string, +) => GraphQLEnumValueConfig; + +type InputObjectTypeConfigMapper = ( + inputObjectConfig: MappedGraphQLInputObjectTypeConfig, +) => MappedGraphQLInputObjectTypeConfig; + +type InputFieldConfigMapper = ( + inputFieldConfig: GraphQLInputFieldConfig, + inputFieldName: string, + inputObjectTypeName: string, +) => GraphQLInputFieldConfig; + +type DirectiveConfigMapper = ( + directiveConfig: GraphQLDirectiveNormalizedConfig, +) => GraphQLDirectiveNormalizedConfig; + +type SchemaConfigMapper = ( + originalSchemaConfig: GraphQLSchemaNormalizedConfig, +) => GraphQLSchemaNormalizedConfig; + +export interface SchemaElementConfigMappers { + [SchemaElementKind.SCALAR]?: ScalarTypeConfigMapper; + [SchemaElementKind.OBJECT]?: ObjectTypeConfigMapper; + [SchemaElementKind.FIELD_DEFINITION]?: FieldConfigMapper; + [SchemaElementKind.ARGUMENT_DEFINITION]?: ArgumentConfigMapper; + [SchemaElementKind.INTERFACE]?: InterfaceTypeConfigMapper; + [SchemaElementKind.UNION]?: UnionTypeConfigMapper; + [SchemaElementKind.ENUM]?: EnumTypeConfigMapper; + [SchemaElementKind.ENUM_VALUE]?: EnumValueConfigMapper; + [SchemaElementKind.INPUT_OBJECT]?: InputObjectTypeConfigMapper; + [SchemaElementKind.INPUT_FIELD_DEFINITION]?: InputFieldConfigMapper; + [SchemaElementKind.DIRECTIVE_DEFINITION]?: DirectiveConfigMapper; + [SchemaElementKind.SCHEMA]?: SchemaConfigMapper; +} + +/** + * @internal + */ +export function mapSchemaConfig( + schemaConfig: GraphQLSchemaNormalizedConfig, + configMappers: (helpers: MapperHelpers) => SchemaElementConfigMappers, +): GraphQLSchemaNormalizedConfig { + const configMappersWithHelpers = configMappers({ + getMappedType, + getMappedNamedType, + addNamedType, + }); + + const mappedTypeMap = new Map(); + for (const type of schemaConfig.types) { + const typeName = type.name; + const mappedNamedType = mapNamedType(type); + if (mappedNamedType) { + mappedTypeMap.set(typeName, mappedNamedType); + } + } + + const mappedDirectives: Array = []; + for (const directive of schemaConfig.directives) { + if (isSpecifiedDirective(directive)) { + // Builtin directives cannot be mapped. + mappedDirectives.push(directive); + continue; + } + + const mappedDirectiveConfig = mapDirective(directive.toConfig()); + if (mappedDirectiveConfig) { + mappedDirectives.push(new GraphQLDirective(mappedDirectiveConfig)); + } + } + + const mappedSchemaConfig = { + ...schemaConfig, + query: schemaConfig.query && getMappedType(schemaConfig.query), + mutation: schemaConfig.mutation && getMappedType(schemaConfig.mutation), + subscription: + schemaConfig.subscription && getMappedType(schemaConfig.subscription), + types: Array.from(mappedTypeMap.values()), + directives: mappedDirectives, + }; + + const schemaMapper = configMappersWithHelpers[SchemaElementKind.SCHEMA]; + + return schemaMapper == null + ? mappedSchemaConfig /* c8 ignore start */ // TODO: add test + : schemaMapper(mappedSchemaConfig); /* c8 ignore stop */ + + function getMappedType(type: T): T { + if (isListType(type)) { + return new GraphQLList(getMappedType(type.ofType)) as T; + } + if (isNonNullType(type)) { + return new GraphQLNonNull(getMappedType(type.ofType)) as T; + } + + return getMappedNamedType(type.name) as T; + } + + function getMappedNamedType(typeName: string): GraphQLNamedType { + const type = stdTypeMap.get(typeName) ?? mappedTypeMap.get(typeName); + invariant(type !== undefined, `Unknown type: "${typeName}".`); + return type; + } + + function addNamedType(type: GraphQLNamedType): void { + mappedTypeMap.set(type.name, type); + } + + function mapNamedType(type: GraphQLNamedType): Maybe { + if (isIntrospectionType(type) || isSpecifiedScalarType(type)) { + // Builtin types cannot be mapped. + return type; + } + + if (isScalarType(type)) { + return mapScalarType(type); + } + if (isObjectType(type)) { + return mapObjectType(type); + } + if (isInterfaceType(type)) { + return mapInterfaceType(type); + } + if (isUnionType(type)) { + return mapUnionType(type); + } + if (isEnumType(type)) { + return mapEnumType(type); + } + if (isInputObjectType(type)) { + return mapInputObjectType(type); + } + /* c8 ignore next 3 */ + // Not reachable, all possible type definition nodes have been considered. + invariant(false, 'Unexpected type: ' + inspect(type)); + } + + function mapScalarType(type: GraphQLScalarType): GraphQLScalarType { + let mappedConfig: Maybe = type.toConfig(); + const mapper = configMappersWithHelpers[SchemaElementKind.SCALAR]; + mappedConfig = + mapper == null + ? mappedConfig /* c8 ignore start */ + : mapper(mappedConfig); /* c8 ignore stop */ + return new GraphQLScalarType(mappedConfig); + } + + function mapObjectType(type: GraphQLObjectType): GraphQLObjectType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + interfaces: () => config.interfaces.map(getMappedType), + fields: () => mapFields(config.fields, type.name), + }; + const mapper = configMappersWithHelpers[SchemaElementKind.OBJECT]; + mappedConfig = + mapper == null + ? mappedConfig /* c8 ignore start */ // TODO: add test + : mapper(mappedConfig); /* c8 ignore stop */ + return new GraphQLObjectType(mappedConfig); + } + + function mapFields( + fieldMap: GraphQLFieldNormalizedConfigMap, + parentTypeName: string, + ): GraphQLFieldNormalizedConfigMap { + const newFieldMap = Object.create(null); + for (const [fieldName, field] of Object.entries(fieldMap)) { + const mappedField = mapField(field, fieldName, parentTypeName); + newFieldMap[fieldName] = mappedField; + } + return newFieldMap; + } + + function mapField( + fieldConfig: GraphQLFieldNormalizedConfig, + fieldName: string, + parentTypeName: string, + ): GraphQLFieldNormalizedConfig { + const mappedConfig = { + ...fieldConfig, + type: getMappedType(fieldConfig.type), + args: mapArgs(fieldConfig.args, parentTypeName, fieldName), + }; + const mapper = configMappersWithHelpers[SchemaElementKind.FIELD_DEFINITION]; + return mapper == null + ? mappedConfig /* c8 ignore start */ // TODO: add test + : mapper(mappedConfig, parentTypeName); /* c8 ignore stop */ + } + + function mapArgs( + argumentMap: GraphQLFieldNormalizedConfigArgumentMap, + fieldOrDirectiveName: string, + parentTypeName?: string | undefined, + ): GraphQLFieldNormalizedConfigArgumentMap { + const newArgumentMap = Object.create(null); + + for (const [argName, arg] of Object.entries(argumentMap)) { + const mappedArg = mapArg(arg, fieldOrDirectiveName, parentTypeName); + newArgumentMap[argName] = mappedArg; + } + + return newArgumentMap; + } + + function mapArg( + argConfig: GraphQLArgumentNormalizedConfig, + fieldOrDirectiveName: string, + parentTypeName?: string | undefined, + ): GraphQLArgumentNormalizedConfig { + const mappedConfig = { + ...argConfig, + type: getMappedType(argConfig.type), + }; + const mapper = + configMappersWithHelpers[SchemaElementKind.ARGUMENT_DEFINITION]; + return mapper == null + ? mappedConfig /* c8 ignore start */ // TODO: add test + : mapper( + mappedConfig, + fieldOrDirectiveName, + parentTypeName, + ); /* c8 ignore stop */ + } + + function mapInterfaceType(type: GraphQLInterfaceType): GraphQLInterfaceType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + interfaces: () => config.interfaces.map(getMappedType), + fields: () => mapFields(config.fields, type.name), + }; + const mapper = configMappersWithHelpers[SchemaElementKind.INTERFACE]; + mappedConfig = + mapper == null + ? mappedConfig /* c8 ignore start */ // TODO: add test + : mapper(mappedConfig); /* c8 ignore stop */ + return new GraphQLInterfaceType(mappedConfig); + } + + function mapUnionType(type: GraphQLUnionType): GraphQLUnionType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + types: () => config.types.map(getMappedType), + }; + const mapper = configMappersWithHelpers[SchemaElementKind.UNION]; + mappedConfig = + mapper == null + ? mappedConfig /* c8 ignore start */ // TODO: add test + : mapper(mappedConfig); /* c8 ignore stop */ + return new GraphQLUnionType(mappedConfig); + } + + function mapEnumType(type: GraphQLEnumType): GraphQLEnumType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + values: () => mapEnumValues(config.values, type.name), + }; + const mapper = configMappersWithHelpers[SchemaElementKind.ENUM]; + mappedConfig = + mapper == null + ? mappedConfig /* c8 ignore start */ // TODO: add test + : mapper(mappedConfig); /* c8 ignore stop */ + return new GraphQLEnumType(mappedConfig); + } + + function mapEnumValues( + valueMap: GraphQLEnumValueConfigMap, + enumName: string, + ): GraphQLEnumValueNormalizedConfigMap { + const newEnumValues = Object.create(null); + for (const [valueName, value] of Object.entries(valueMap)) { + const mappedValue = mapEnumValue(value, valueName, enumName); + newEnumValues[valueName] = mappedValue; + } + return newEnumValues; + } + + function mapEnumValue( + valueConfig: GraphQLEnumValueConfig, + valueName: string, + enumName: string, + ): GraphQLEnumValueConfig { + const mappedConfig = { ...valueConfig }; + const mapper = configMappersWithHelpers[SchemaElementKind.ENUM_VALUE]; + return mapper == null + ? mappedConfig /* c8 ignore start */ // TODO: add test + : mapper(mappedConfig, valueName, enumName); /* c8 ignore stop */ + } + + function mapInputObjectType( + type: GraphQLInputObjectType, + ): GraphQLInputObjectType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + fields: () => mapInputFields(config.fields, type.name), + }; + const mapper = configMappersWithHelpers[SchemaElementKind.INPUT_OBJECT]; + mappedConfig = + mapper == null + ? mappedConfig /* c8 ignore start */ // TODO: add test + : mapper(mappedConfig); /* c8 ignore stop */ + return new GraphQLInputObjectType(mappedConfig); + } + + function mapInputFields( + inputFieldMap: GraphQLInputFieldNormalizedConfigMap, + inputObjectTypeName: string, + ): GraphQLInputFieldNormalizedConfigMap { + const newInputFieldMap = Object.create(null); + for (const [fieldName, field] of Object.entries(inputFieldMap)) { + const mappedField = mapInputField(field, fieldName, inputObjectTypeName); + newInputFieldMap[fieldName] = mappedField; + } + return newInputFieldMap; + } + + function mapInputField( + inputFieldConfig: GraphQLInputFieldConfig, + inputFieldName: string, + inputObjectTypeName: string, + ): GraphQLInputFieldConfig { + const mappedConfig = { + ...inputFieldConfig, + type: getMappedType(inputFieldConfig.type), + }; + const mapper = + configMappersWithHelpers[SchemaElementKind.INPUT_FIELD_DEFINITION]; + return mapper == null + ? mappedConfig /* c8 ignore start */ // TODO: add test + : mapper( + mappedConfig, + inputFieldName, + inputObjectTypeName, + ); /* c8 ignore stop */ + } + + function mapDirective( + config: GraphQLDirectiveNormalizedConfig, + ): Maybe { + const mappedConfig = { + ...config, + args: mapArgs(config.args, config.name, undefined), + }; + const mapper = + configMappersWithHelpers[SchemaElementKind.DIRECTIVE_DEFINITION]; + return mapper == null ? mappedConfig : mapper(mappedConfig); + } +}