From b978613e87ab18e5647a1dcf6339e2b9881c6ce8 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 26 Nov 2024 11:30:08 +0200 Subject: [PATCH 1/3] introduce mapSchemaConfig motivation: 1. can be used to extract common logic from extendSchemaImpl and lexicographicSortSchema 2. can be used further enhance extendSchemaImpl to take resolvers 3. can be exposed to provide a generic safe mapSchemaConfig --- src/type/definition.ts | 57 +- src/type/directives.ts | 6 +- .../__tests__/mapSchemaConfig-test.ts | 919 ++++++++++++++++++ src/utilities/mapSchemaConfig.ts | 464 +++++++++ 4 files changed, 1432 insertions(+), 14 deletions(-) create mode 100644 src/utilities/__tests__/mapSchemaConfig-test.ts create mode 100644 src/utilities/mapSchemaConfig.ts 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/__tests__/mapSchemaConfig-test.ts b/src/utilities/__tests__/mapSchemaConfig-test.ts new file mode 100644 index 0000000000..e443dadfa4 --- /dev/null +++ b/src/utilities/__tests__/mapSchemaConfig-test.ts @@ -0,0 +1,919 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { dedentString } from '../../__testUtils__/dedent.js'; + +import { GraphQLObjectType } from '../../type/definition.js'; +import type { GraphQLSchemaNormalizedConfig } from '../../type/schema.js'; +import { GraphQLSchema } from '../../type/schema.js'; + +import { buildSchema } from '../buildASTSchema.js'; +import type { + ConfigMapperMap, + MappedSchemaContext, +} from '../mapSchemaConfig.js'; +import { mapSchemaConfig, SchemaElementKind } from '../mapSchemaConfig.js'; +import { printSchema } from '../printSchema.js'; + +function expectSchemaMapping( + schemaConfig: GraphQLSchemaNormalizedConfig, + configMapperMapFn: (context: MappedSchemaContext) => ConfigMapperMap, + expected: string, +) { + const newSchemaConfig = mapSchemaConfig(schemaConfig, configMapperMapFn); + expect(printSchema(new GraphQLSchema(newSchemaConfig))).to.equal( + dedentString(expected), + ); +} + +describe('mapSchemaConfig', () => { + it('returns the original config when no mappers are included', () => { + const sdl = 'type Query'; + const schemaConfig = buildSchema(sdl).toConfig(); + expectSchemaMapping(schemaConfig, () => ({}), sdl); + }); + + describe('scalar mapping', () => { + it('can map scalar types', () => { + const sdl = 'scalar SomeScalar'; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.SCALAR]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + """Some description""" + scalar SomeScalar + `, + ); + }); + }); + + describe('argument mapping', () => { + it('can map arguments', () => { + const sdl = ` + type SomeType { + field(arg: String): String + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.ARGUMENT]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + type SomeType { + field( + """Some description""" + arg: String + ): String + } + `, + ); + }); + }); + + describe('field mapping', () => { + it('can map fields', () => { + const sdl = ` + type SomeType { + field: String + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.FIELD]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + type SomeType { + """Some description""" + field: String + } + `, + ); + }); + + it('maps fields after mapping arguments', () => { + const sdl = ` + type SomeType { + field(arg: String): String + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.ARGUMENT]: (config) => ({ + ...config, + description: 'Some argument description', + }), + [SchemaElementKind.FIELD]: (config) => { + expect(config.args.arg.description).to.equal( + 'Some argument description', + ); + return { + ...config, + description: 'Some field description', + }; + }, + }), + ` + type SomeType { + """Some field description""" + field( + """Some argument description""" + arg: String + ): String + } + `, + ); + }); + }); + + describe('object type mapping', () => { + it('can map object types', () => { + const sdl = 'type SomeType'; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.OBJECT]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + """Some description""" + type SomeType + `, + ); + }); + + it('maps object types after mapping fields', () => { + const sdl = ` + type SomeType { + field: String + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.FIELD]: (config) => ({ + ...config, + description: 'Some field description', + }), + [SchemaElementKind.OBJECT]: (config) => { + expect(config.fields().field.description).to.equal( + 'Some field description', + ); + return { + ...config, + description: 'Some object description', + }; + }, + }), + ` + """Some object description""" + type SomeType { + """Some field description""" + field: String + } + `, + ); + }); + + it('maps object types after mapping interfaces', () => { + const sdl = ` + interface SomeInterface + type SomeType implements SomeInterface + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.INTERFACE]: (config) => ({ + ...config, + description: 'Some interface description', + }), + [SchemaElementKind.OBJECT]: (config) => { + expect(config.interfaces()[0].description).to.equal( + 'Some interface description', + ); + return { + ...config, + description: 'Some object description', + }; + }, + }), + ` + """Some interface description""" + interface SomeInterface + + """Some object description""" + type SomeType implements SomeInterface + `, + ); + }); + }); + + describe('interface type mapping', () => { + it('can map interface types', () => { + const sdl = 'interface SomeInterface'; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.INTERFACE]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + """Some description""" + interface SomeInterface + `, + ); + }); + + it('maps interface types after mapping fields', () => { + const sdl = ` + interface SomeInterface { + field: String + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.FIELD]: (config) => ({ + ...config, + description: 'Some field description', + }), + [SchemaElementKind.INTERFACE]: (config) => { + expect(config.fields().field.description).to.equal( + 'Some field description', + ); + return { + ...config, + description: 'Some interface description', + }; + }, + }), + ` + """Some interface description""" + interface SomeInterface { + """Some field description""" + field: String + } + `, + ); + }); + + it('maps interface types after mapping parent interfaces', () => { + const sdl = ` + interface SomeParentInterface + interface SomeInterface implements SomeParentInterface + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.INTERFACE]: (config) => { + if (config.name === 'SomeInterface') { + expect(config.interfaces()[0].description).to.equal( + 'Some interface description', + ); + } + return { + ...config, + description: 'Some interface description', + }; + }, + }), + ` + """Some interface description""" + interface SomeParentInterface + + """Some interface description""" + interface SomeInterface implements SomeParentInterface + `, + ); + }); + }); + + describe('union type mapping', () => { + it('can map union types', () => { + const sdl = 'union SomeUnion'; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.UNION]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + """Some description""" + union SomeUnion + `, + ); + }); + + it('maps union types after mapping types', () => { + const sdl = ` + type SomeType + union SomeUnion = SomeType + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.OBJECT]: (config) => ({ + ...config, + description: 'Some type description', + }), + [SchemaElementKind.UNION]: (config) => { + expect(config.types()[0].description).to.equal( + 'Some type description', + ); + return { + ...config, + description: 'Some union description', + }; + }, + }), + ` + """Some type description""" + type SomeType + + """Some union description""" + union SomeUnion = SomeType + `, + ); + }); + }); + + describe('enum value mapping', () => { + it('can map enum values', () => { + const sdl = ` + enum SomeEnum { + SOME_VALUE + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.ENUM_VALUE]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + enum SomeEnum { + """Some description""" + SOME_VALUE + } + `, + ); + }); + }); + + describe('enum type mapping', () => { + it('can map enum types', () => { + const sdl = 'enum SomeEnum'; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.ENUM]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + """Some description""" + enum SomeEnum + `, + ); + }); + + it('maps enum types after mapping values', () => { + const sdl = ` + enum SomeEnum { + SOME_VALUE + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.ENUM_VALUE]: (config) => ({ + ...config, + description: 'Some value description', + }), + [SchemaElementKind.ENUM]: (config) => { + expect(config.values().SOME_VALUE.description).to.equal( + 'Some value description', + ); + return { + ...config, + description: 'Some enum description', + }; + }, + }), + ` + """Some enum description""" + enum SomeEnum { + """Some value description""" + SOME_VALUE + } + `, + ); + }); + }); + + describe('input field mapping', () => { + it('can map input fields', () => { + const sdl = ` + input SomeInput { + field: String + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.INPUT_FIELD]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + input SomeInput { + """Some description""" + field: String + } + `, + ); + }); + }); + + describe('input object type mapping', () => { + it('can map input object types', () => { + const sdl = 'input SomeInput'; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.INPUT_OBJECT]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + """Some description""" + input SomeInput + `, + ); + }); + + it('maps input object types after mapping input fields', () => { + const sdl = ` + input SomeInput { + field: String + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.INPUT_FIELD]: (config) => ({ + ...config, + description: 'Some input field description', + }), + [SchemaElementKind.INPUT_OBJECT]: (config) => { + expect(config.fields().field.description).to.equal( + 'Some input field description', + ); + return { + ...config, + description: 'Some input object description', + }; + }, + }), + ` + """Some input object description""" + input SomeInput { + """Some input field description""" + field: String + } + `, + ); + }); + }); + + describe('directive mapping', () => { + it('can map directives', () => { + const sdl = ` + directive @SomeDirective on FIELD + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.DIRECTIVE]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + """Some description""" + directive @SomeDirective on FIELD + `, + ); + }); + + it('maps directives after mapping arguments', () => { + const sdl = ` + directive @SomeDirective(arg: String) on FIELD + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.ARGUMENT]: (config) => ({ + ...config, + description: 'Some argument description', + }), + [SchemaElementKind.DIRECTIVE]: (config) => { + expect(config.args.arg.description).to.equal( + 'Some argument description', + ); + return { + ...config, + description: 'Some directive description', + }; + }, + }), + ` + """Some directive description""" + directive @SomeDirective( + """Some argument description""" + arg: String + ) on FIELD + `, + ); + }); + }); + + describe('schema mapping', () => { + it('can map the schema', () => { + const sdl = ` + type Query + + type Mutation + + type Subscription + + directive @SomeDirective on FIELD + + scalar SomeScalar + + type SomeType { + field: String + } + + interface SomeInterface + + union SomeUnion + + enum SomeEnum { + SOME_VALUE + } + + input SomeInput { + field: String + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.SCHEMA]: (config) => ({ + ...config, + description: 'Some description', + }), + }), + ` + """Some description""" + schema { + query: Query + mutation: Mutation + subscription: Subscription + } + + directive @SomeDirective on FIELD + + type Query + + type Mutation + + type Subscription + + scalar SomeScalar + + type SomeType { + field: String + } + + interface SomeInterface + + union SomeUnion + + enum SomeEnum { + SOME_VALUE + } + + input SomeInput { + field: String + } + `, + ); + }); + + it('maps the schema after mapping types and directives', () => { + const sdl = ` + type Query + + type Mutation + + type Subscription + + directive @SomeDirective on FIELD + + scalar SomeScalar + + type SomeType { + field: String + } + + interface SomeInterface + + union SomeUnion + + enum SomeEnum { + SOME_VALUE + } + + input SomeInput { + field: String + } + `; + + const schemaConfig = buildSchema(sdl).toConfig(); + + expectSchemaMapping( + schemaConfig, + () => ({ + [SchemaElementKind.DIRECTIVE]: (config) => ({ + ...config, + description: 'Some directive description', + }), + [SchemaElementKind.SCALAR]: (config) => ({ + ...config, + description: 'Some scalar description', + }), + [SchemaElementKind.OBJECT]: (config) => ({ + ...config, + description: 'Some object description', + }), + [SchemaElementKind.INTERFACE]: (config) => ({ + ...config, + description: 'Some interface description', + }), + [SchemaElementKind.UNION]: (config) => ({ + ...config, + description: 'Some union description', + }), + [SchemaElementKind.ENUM]: (config) => ({ + ...config, + description: 'Some enum description', + }), + [SchemaElementKind.INPUT_OBJECT]: (config) => ({ + ...config, + description: 'Some input object description', + }), + [SchemaElementKind.SCHEMA]: (config) => { + for (const directive of config.directives) { + if (directive.name === 'SomeDirective') { + expect(directive.description).to.equal( + 'Some directive description', + ); + } + } + + for (const type of config.types) { + switch (type.name) { + case 'SomeScalar': + expect(type.description).to.equal('Some scalar description'); + break; + case 'SomeType': + expect(type.description).to.equal('Some object description'); + break; + case 'SomeInterface': + expect(type.description).to.equal( + 'Some interface description', + ); + break; + case 'SomeUnion': + expect(type.description).to.equal('Some union description'); + break; + case 'SomeEnum': + expect(type.description).to.equal('Some enum description'); + break; + case 'SomeInput': + expect(type.description).to.equal( + 'Some input object description', + ); + break; + } + } + + return { + ...config, + description: 'Some schema description', + }; + }, + }), + ` + """Some schema description""" + schema { + query: Query + mutation: Mutation + subscription: Subscription + } + + """Some directive description""" + directive @SomeDirective on FIELD + + """Some object description""" + type Query + + """Some object description""" + type Mutation + + """Some object description""" + type Subscription + + """Some scalar description""" + scalar SomeScalar + + """Some object description""" + type SomeType { + field: String + } + + """Some interface description""" + interface SomeInterface + + """Some union description""" + union SomeUnion + + """Some enum description""" + enum SomeEnum { + SOME_VALUE + } + + """Some input object description""" + input SomeInput { + field: String + } + `, + ); + }); + }); + + describe('schema context', () => { + it('allows access to the final mapped named type via getNamedType()', () => { + const sdl = ` + """Some description""" + type SomeType + `; + + const schema = buildSchema(sdl); + const schemaConfig = schema.toConfig(); + const someType = schema.getType('SomeType') as GraphQLObjectType; + + expectSchemaMapping( + schemaConfig, + ({ getNamedType }) => { + return { + [SchemaElementKind.OBJECT]: (config) => ({ + ...config, + fields: () => { + expectMappedSomeType(); + return config.fields(); + }, + }), + [SchemaElementKind.SCHEMA]: (config) => { + expectMappedSomeType(); + return config; + }, + }; + + function expectMappedSomeType() { + const mappedType = getNamedType(someType.name); + expect(mappedType).not.to.equal(someType); + expect(mappedType.description).to.equal(someType.description); + } + }, + sdl, + ); + }); + + it('allows adding a named type via setNamedType() and retrieving the new list via getNamedTypes', () => { + const sdl = 'type SomeType'; + + const schema = buildSchema(sdl); + const schemaConfig = schema.toConfig(); + + expectSchemaMapping( + schemaConfig, + ({ setNamedType, getNamedTypes }) => ({ + [SchemaElementKind.SCHEMA]: (config) => { + setNamedType( + new GraphQLObjectType({ name: 'AnotherType', fields: {} }), + ); + return { + ...config, + types: getNamedTypes(), + }; + }, + }), + ` + type SomeType + + type AnotherType + `, + ); + }); + }); +}); diff --git a/src/utilities/mapSchemaConfig.ts b/src/utilities/mapSchemaConfig.ts new file mode 100644 index 0000000000..6246f2013e --- /dev/null +++ b/src/utilities/mapSchemaConfig.ts @@ -0,0 +1,464 @@ +import { inspect } from '../jsutils/inspect.js'; +import { invariant } from '../jsutils/invariant.js'; +import type { Maybe } from '../jsutils/Maybe.js'; + +import type { + GraphQLArgumentNormalizedConfig, + GraphQLEnumTypeNormalizedConfig, + GraphQLEnumValueConfig, + GraphQLFieldNormalizedConfig, + GraphQLFieldNormalizedConfigArgumentMap, + GraphQLFieldNormalizedConfigMap, + GraphQLInputFieldConfig, + 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: 'FIELD' as const, + ARGUMENT: 'ARGUMENT' 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: 'INPUT_FIELD' as const, + DIRECTIVE: 'DIRECTIVE' as const, +} as const; +// eslint-disable-next-line @typescript-eslint/no-redeclare +type SchemaElementKind = + (typeof SchemaElementKind)[keyof typeof SchemaElementKind]; + +export interface MappedSchemaContext { + getNamedType: (typeName: string) => GraphQLNamedType; + setNamedType: (type: GraphQLNamedType) => void; + getNamedTypes: () => ReadonlyArray; +} + +type GraphQLScalarTypeMappedConfig = GraphQLScalarTypeNormalizedConfig< + any, + any +>; + +type EnsureThunks = { + [K in keyof T]: K extends ThunkFields ? () => T[K] : T[K]; +}; + +type GraphQLObjectTypeMappedConfig = EnsureThunks< + GraphQLObjectTypeNormalizedConfig, + 'interfaces' | 'fields' +>; +type GraphQLInterfaceTypeMappedConfig = EnsureThunks< + GraphQLInterfaceTypeNormalizedConfig, + 'interfaces' | 'fields' +>; +type GraphQLUnionTypeMappedConfig = EnsureThunks< + GraphQLUnionTypeNormalizedConfig, + 'types' +>; +type GraphQLEnumTypeMappedConfig = EnsureThunks< + GraphQLEnumTypeNormalizedConfig, + 'values' +>; +type GraphQLInputObjectTypeMappedConfig = EnsureThunks< + GraphQLInputObjectTypeNormalizedConfig, + 'fields' +>; + +type ScalarTypeConfigMapper = ( + scalarConfig: GraphQLScalarTypeMappedConfig, +) => GraphQLScalarTypeMappedConfig; + +type ObjectTypeConfigMapper = ( + objectConfig: GraphQLObjectTypeMappedConfig, +) => GraphQLObjectTypeMappedConfig; + +type FieldConfigMapper = ( + fieldConfig: GraphQLFieldNormalizedConfig, + parentTypeName: string, +) => GraphQLFieldNormalizedConfig; + +type ArgumentConfigMapper = ( + argConfig: GraphQLArgumentNormalizedConfig, + fieldOrDirectiveName: string, + parentTypeName?: string | undefined, +) => GraphQLArgumentNormalizedConfig; + +type InterfaceTypeConfigMapper = ( + interfaceConfig: GraphQLInterfaceTypeMappedConfig, +) => GraphQLInterfaceTypeMappedConfig; + +type UnionTypeConfigMapper = ( + unionConfig: GraphQLUnionTypeMappedConfig, +) => GraphQLUnionTypeMappedConfig; + +type EnumTypeConfigMapper = ( + enumConfig: GraphQLEnumTypeMappedConfig, +) => GraphQLEnumTypeMappedConfig; + +type EnumValueConfigMapper = ( + enumValueConfig: GraphQLEnumValueConfig, + valueName: string, + enumName: string, +) => GraphQLEnumValueConfig; + +type InputObjectTypeConfigMapper = ( + inputObjectConfig: GraphQLInputObjectTypeMappedConfig, +) => GraphQLInputObjectTypeMappedConfig; + +type InputFieldConfigMapper = ( + inputFieldConfig: GraphQLInputFieldConfig, + inputFieldName: string, + inputObjectTypeName: string, +) => GraphQLInputFieldConfig; + +type DirectiveConfigMapper = ( + directiveConfig: GraphQLDirectiveNormalizedConfig, +) => GraphQLDirectiveNormalizedConfig; + +type SchemaConfigMapper = ( + originalSchemaConfig: GraphQLSchemaNormalizedConfig, +) => GraphQLSchemaNormalizedConfig; + +export interface ConfigMapperMap { + [SchemaElementKind.SCALAR]?: ScalarTypeConfigMapper; + [SchemaElementKind.OBJECT]?: ObjectTypeConfigMapper; + [SchemaElementKind.FIELD]?: FieldConfigMapper; + [SchemaElementKind.ARGUMENT]?: ArgumentConfigMapper; + [SchemaElementKind.INTERFACE]?: InterfaceTypeConfigMapper; + [SchemaElementKind.UNION]?: UnionTypeConfigMapper; + [SchemaElementKind.ENUM]?: EnumTypeConfigMapper; + [SchemaElementKind.ENUM_VALUE]?: EnumValueConfigMapper; + [SchemaElementKind.INPUT_OBJECT]?: InputObjectTypeConfigMapper; + [SchemaElementKind.INPUT_FIELD]?: InputFieldConfigMapper; + [SchemaElementKind.DIRECTIVE]?: DirectiveConfigMapper; + [SchemaElementKind.SCHEMA]?: SchemaConfigMapper; +} + +/** + * @internal + */ +export function mapSchemaConfig( + schemaConfig: GraphQLSchemaNormalizedConfig, + configMapperMapFn: (context: MappedSchemaContext) => ConfigMapperMap, +): GraphQLSchemaNormalizedConfig { + const configMapperMap = configMapperMapFn({ + getNamedType, + setNamedType, + getNamedTypes, + }); + + 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 && + (getNamedType(schemaConfig.query.name) as GraphQLObjectType), + mutation: + schemaConfig.mutation && + (getNamedType(schemaConfig.mutation.name) as GraphQLObjectType), + subscription: + schemaConfig.subscription && + (getNamedType(schemaConfig.subscription.name) as GraphQLObjectType), + types: Array.from(mappedTypeMap.values()), + directives: mappedDirectives, + }; + + const schemaMapper = configMapperMap[SchemaElementKind.SCHEMA]; + + return schemaMapper == null + ? mappedSchemaConfig + : schemaMapper(mappedSchemaConfig); + + function getType(type: T): T { + if (isListType(type)) { + return new GraphQLList(getType(type.ofType)) as T; + } + if (isNonNullType(type)) { + return new GraphQLNonNull(getType(type.ofType)) as T; + } + + return getNamedType(type.name) as T; + } + + function getNamedType(typeName: string): GraphQLNamedType { + const type = stdTypeMap.get(typeName) ?? mappedTypeMap.get(typeName); + invariant(type !== undefined, `Unknown type: "${typeName}".`); + return type; + } + + function setNamedType(type: GraphQLNamedType): void { + mappedTypeMap.set(type.name, type); + } + + function getNamedTypes(): ReadonlyArray { + return Array.from(mappedTypeMap.values()); + } + + 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 = configMapperMap[SchemaElementKind.SCALAR]; + mappedConfig = mapper == null ? mappedConfig : mapper(mappedConfig); + return new GraphQLScalarType(mappedConfig); + } + + function mapObjectType(type: GraphQLObjectType): GraphQLObjectType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + interfaces: () => + config.interfaces.map( + (iface) => getNamedType(iface.name) as GraphQLInterfaceType, + ), + fields: () => mapFields(config.fields, type.name), + }; + const mapper = configMapperMap[SchemaElementKind.OBJECT]; + mappedConfig = mapper == null ? mappedConfig : mapper(mappedConfig); + return new GraphQLObjectType(mappedConfig); + } + + function mapFields( + fieldMap: GraphQLFieldNormalizedConfigMap, + parentTypeName: string, + ): GraphQLFieldNormalizedConfigMap { + const newFieldMap = Object.create(null); + for (const [fieldName, field] of Object.entries(fieldMap)) { + let mappedField = { + ...field, + type: getType(field.type), + args: mapArgs(field.args, parentTypeName, fieldName), + }; + const mapper = configMapperMap[SchemaElementKind.FIELD]; + if (mapper) { + mappedField = mapper(mappedField, parentTypeName); + } + newFieldMap[fieldName] = mappedField; + } + return newFieldMap; + } + + function mapArgs( + argumentMap: GraphQLFieldNormalizedConfigArgumentMap, + fieldOrDirectiveName: string, + parentTypeName?: string | undefined, + ): GraphQLFieldNormalizedConfigArgumentMap { + const newArgumentMap = Object.create(null); + + for (const [argName, arg] of Object.entries(argumentMap)) { + let mappedArg = { + ...arg, + type: getType(arg.type), + }; + const mapper = configMapperMap[SchemaElementKind.ARGUMENT]; + if (mapper) { + mappedArg = mapper(mappedArg, fieldOrDirectiveName, parentTypeName); + } + newArgumentMap[argName] = mappedArg; + } + + return newArgumentMap; + } + + function mapInterfaceType(type: GraphQLInterfaceType): GraphQLInterfaceType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + interfaces: () => + config.interfaces.map( + (iface) => getNamedType(iface.name) as GraphQLInterfaceType, + ), + fields: () => mapFields(config.fields, type.name), + }; + const mapper = configMapperMap[SchemaElementKind.INTERFACE]; + mappedConfig = mapper == null ? mappedConfig : mapper(mappedConfig); + return new GraphQLInterfaceType(mappedConfig); + } + + function mapUnionType(type: GraphQLUnionType): GraphQLUnionType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + types: () => + config.types.map( + (memberType) => getNamedType(memberType.name) as GraphQLObjectType, + ), + }; + const mapper = configMapperMap[SchemaElementKind.UNION]; + mappedConfig = mapper == null ? mappedConfig : mapper(mappedConfig); + return new GraphQLUnionType(mappedConfig); + } + + function mapEnumType(type: GraphQLEnumType): GraphQLEnumType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + values: () => { + const newEnumValues = Object.create(null); + for (const [valueName, value] of Object.entries(config.values)) { + const mappedValue = mapEnumValue(value, valueName, type.name); + newEnumValues[valueName] = mappedValue; + } + return newEnumValues; + }, + }; + const mapper = configMapperMap[SchemaElementKind.ENUM]; + mappedConfig = mapper == null ? mappedConfig : mapper(mappedConfig); + return new GraphQLEnumType(mappedConfig); + } + + function mapEnumValue( + valueConfig: GraphQLEnumValueConfig, + valueName: string, + enumName: string, + ): GraphQLEnumValueConfig { + const mappedConfig = { ...valueConfig }; + const mapper = configMapperMap[SchemaElementKind.ENUM_VALUE]; + return mapper == null + ? mappedConfig + : mapper(mappedConfig, valueName, enumName); + } + + function mapInputObjectType( + type: GraphQLInputObjectType, + ): GraphQLInputObjectType { + const config = type.toConfig(); + let mappedConfig: Maybe = { + ...config, + fields: () => { + const newInputFieldMap = Object.create(null); + for (const [fieldName, field] of Object.entries(config.fields)) { + const mappedField = mapInputField(field, fieldName, type.name); + newInputFieldMap[fieldName] = mappedField; + } + return newInputFieldMap; + }, + }; + const mapper = configMapperMap[SchemaElementKind.INPUT_OBJECT]; + mappedConfig = mapper == null ? mappedConfig : mapper(mappedConfig); + return new GraphQLInputObjectType(mappedConfig); + } + + function mapInputField( + inputFieldConfig: GraphQLInputFieldConfig, + inputFieldName: string, + inputObjectTypeName: string, + ): GraphQLInputFieldConfig { + const mappedConfig = { + ...inputFieldConfig, + type: getType(inputFieldConfig.type), + }; + const mapper = configMapperMap[SchemaElementKind.INPUT_FIELD]; + return mapper == null + ? mappedConfig + : mapper(mappedConfig, inputFieldName, inputObjectTypeName); + } + + function mapDirective( + config: GraphQLDirectiveNormalizedConfig, + ): Maybe { + const mappedConfig = { + ...config, + args: mapArgs(config.args, config.name, undefined), + }; + const mapper = configMapperMap[SchemaElementKind.DIRECTIVE]; + return mapper == null ? mappedConfig : mapper(mappedConfig); + } +} + +const stdTypeMap = new Map( + [...specifiedScalarTypes, ...introspectionTypes].map((type) => [ + type.name, + type, + ]), +); From c02eff899612d916a88ff6e13b74b7f0dedfd86d Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 26 Nov 2024 11:30:48 +0200 Subject: [PATCH 2/3] simplify lexicographicSortSchema with mapSchemaConfig --- src/utilities/lexicographicSortSchema.ts | 189 +++++------------------ 1 file changed, 39 insertions(+), 150 deletions(-) diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts index e3f25e1c4a..639aa6e01e 100644 --- a/src/utilities/lexicographicSortSchema.ts +++ b/src/utilities/lexicographicSortSchema.ts @@ -1,173 +1,62 @@ -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 { GraphQLSchema } from '../type/schema.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({ - ...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 sortDirective(directive: GraphQLDirective) { - const config = directive.toConfig(); - return new GraphQLDirective({ - ...config, - locations: sortBy(config.locations, (x) => x), - args: sortArgs(config.args), - }); - } - - function sortArgs(args: GraphQLFieldConfigArgumentMap) { - return sortObjMap(args, (arg) => ({ - ...arg, - type: replaceType(arg.type), - })); - } - - function sortFields(fieldsMap: GraphQLFieldConfigMap) { - return sortObjMap(fieldsMap, (field) => ({ - ...field, - type: replaceType(field.type), - args: field.args && sortArgs(field.args), - })); - } - - function sortInputFields(fieldsMap: GraphQLInputFieldConfigMap) { - return sortObjMap(fieldsMap, (field) => ({ - ...field, - type: replaceType(field.type), - })); - } - - function sortTypes( - array: ReadonlyArray, - ): Array { - return sortByName(array).map(replaceNamedType); - } - - function sortNamedType(type: GraphQLNamedType): GraphQLNamedType { - if (isScalarType(type) || isIntrospectionType(type)) { - return type; - } - if (isObjectType(type)) { - const config = type.toConfig(); - return new GraphQLObjectType({ + return new GraphQLSchema( + mapSchemaConfig(schema.toConfig(), () => ({ + [SchemaElementKind.OBJECT]: (config) => ({ ...config, - interfaces: () => sortTypes(config.interfaces), - fields: () => sortFields(config.fields), - }); - } - if (isInterfaceType(type)) { - const config = type.toConfig(); - return new GraphQLInterfaceType({ + interfaces: () => sortByName(config.interfaces()), + fields: () => sortObjMap(config.fields()), + }), + [SchemaElementKind.FIELD]: (config) => ({ ...config, - interfaces: () => sortTypes(config.interfaces), - fields: () => sortFields(config.fields), - }); - } - if (isUnionType(type)) { - const config = type.toConfig(); - return new GraphQLUnionType({ + args: sortObjMap(config.args), + }), + [SchemaElementKind.INTERFACE]: (config) => ({ ...config, - types: () => sortTypes(config.types), - }); - } - if (isEnumType(type)) { - const config = type.toConfig(); - return new GraphQLEnumType({ + interfaces: () => sortByName(config.interfaces()), + fields: () => sortObjMap(config.fields()), + }), + [SchemaElementKind.UNION]: (config) => ({ ...config, - values: sortObjMap(config.values, (value) => value), - }); - } - if (isInputObjectType(type)) { - const config = type.toConfig(); - return new GraphQLInputObjectType({ + types: () => sortByName(config.types()), + }), + [SchemaElementKind.ENUM]: (config) => ({ ...config, - fields: () => sortInputFields(config.fields), - }); - } - /* c8 ignore next 3 */ - // Not reachable, all possible types have been considered. - invariant(false, 'Unexpected type: ' + inspect(type)); - } + values: () => sortObjMap(config.values()), + }), + [SchemaElementKind.INPUT_OBJECT]: (config) => ({ + ...config, + fields: () => sortObjMap(config.fields()), + }), + [SchemaElementKind.DIRECTIVE]: (config) => ({ + ...config, + locations: sortBy(config.locations, (x) => x), + args: sortObjMap(config.args), + }), + [SchemaElementKind.SCHEMA]: (config) => ({ + ...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; } From d4b01520168821fa424b894811606dc83303f2a8 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 26 Nov 2024 11:31:08 +0200 Subject: [PATCH 3/3] simplify extendSchemaImpl with mapSchemaConfig --- src/utilities/extendSchema.ts | 809 +++++++++++++++------------------- 1 file changed, 346 insertions(+), 463 deletions(-) diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 36e357b2ba..5abd510189 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,8 @@ import { assertValidSDLExtension } from '../validation/validate.js'; import { getDirectiveValues } from '../execution/values.js'; +import { mapSchemaConfig, SchemaElementKind } from './mapSchemaConfig.js'; + interface Options extends GraphQLSchemaValidationOptions { /** * Set to true to assume the SDL is valid. @@ -214,478 +197,378 @@ export function extendSchemaImpl( 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)); - } - - 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), - }); - } - - function extendScalarType(type: GraphQLScalarType): GraphQLScalarType { - const config = type.toConfig(); - const extensions = scalarExtensions.get(config.name) ?? []; - - let specifiedByURL = config.specifiedByURL; - for (const extensionNode of extensions) { - specifiedByURL = getSpecifiedByURL(extensionNode) ?? specifiedByURL; - } - - return new GraphQLScalarType({ - ...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 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 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 extendField( - field: GraphQLFieldConfig, - ): GraphQLFieldConfig { + return mapSchemaConfig(schemaConfig, (context) => { + const { getNamedType, setNamedType, getNamedTypes } = context; return { - ...field, - type: replaceType(field.type), - args: field.args && mapValue(field.args, extendArg), - }; - } + [SchemaElementKind.SCHEMA]: (config) => { + for (const typeNode of typeDefs) { + const type = + stdTypeMap.get(typeNode.name.value) ?? buildNamedType(typeNode); + setNamedType(type); + } + + const operationTypes = { + // Get the extended root operation types. + query: + config.query && + (getNamedType(config.query.name) as GraphQLObjectType), + mutation: + config.mutation && + (getNamedType(config.mutation.name) as GraphQLObjectType), + subscription: + config.subscription && + (getNamedType(config.subscription.name) as GraphQLObjectType), + // Then, incorporate schema definition and all schema extensions. + ...(schemaDef && getOperationTypes([schemaDef])), + ...getOperationTypes(schemaExtensions), + }; - function extendArg(arg: GraphQLArgumentConfig) { - return { - ...arg, - type: replaceType(arg.type), + // Then produce and return a Schema config with these types. + return { + description: schemaDef?.description?.value ?? config.description, + ...operationTypes, + types: getNamedTypes(), + directives: [ + ...config.directives, + ...directiveDefs.map(buildDirective), + ], + extensions: config.extensions, + astNode: schemaDef ?? config.astNode, + extensionASTNodes: config.extensionASTNodes.concat(schemaExtensions), + assumeValid: options?.assumeValid ?? false, + }; + }, + [SchemaElementKind.INPUT_OBJECT]: (config) => { + const extensions = inputObjectExtensions.get(config.name) ?? []; + return { + ...config, + fields: () => ({ + ...config.fields(), + ...buildInputFieldMap(extensions), + }), + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + }, + [SchemaElementKind.ENUM]: (config) => { + const extensions = enumExtensions.get(config.name) ?? []; + return { + ...config, + values: () => ({ + ...config.values(), + ...buildEnumValueMap(extensions), + }), + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + }, + [SchemaElementKind.SCALAR]: (config) => { + 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), + }; + }, + [SchemaElementKind.OBJECT]: (config) => { + const extensions = objectExtensions.get(config.name) ?? []; + return { + ...config, + interfaces: () => [ + ...config.interfaces(), + ...buildInterfaces(extensions), + ], + fields: () => ({ + ...config.fields(), + ...buildFieldMap(extensions), + }), + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + }, + [SchemaElementKind.INTERFACE]: (config) => { + const extensions = interfaceExtensions.get(config.name) ?? []; + return { + ...config, + interfaces: () => [ + ...config.interfaces(), + ...buildInterfaces(extensions), + ], + fields: () => ({ + ...config.fields(), + ...buildFieldMap(extensions), + }), + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }; + }, + [SchemaElementKind.UNION]: (config) => { + const extensions = unionExtensions.get(config.name) ?? []; + return { + ...config, + types: () => [...config.types(), ...buildUnionTypes(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] = namedTypeFromAST( + operationType.type, + ); + } } - } - return opTypes; - } - - function getNamedType(node: NamedTypeNode): GraphQLNamedType { - const name = node.name.value; - const type = stdTypeMap.get(name) ?? typeMap.get(name); - - if (type === undefined) { - throw new Error(`Unknown type: "${name}".`); + return opTypes; } - return type; - } - 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)); + function namedTypeFromAST(node: NamedTypeNode): GraphQLNamedType { + const name = node.name.value; + const type = getNamedType(name); + invariant(type !== undefined, `Unknown type: "${name}".`); + return type; } - return getNamedType(node); - } - 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 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] = { - // 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, - }; + function typeFromAST(node: TypeNode): GraphQLType { + if (node.kind === Kind.LIST_TYPE) { + return new GraphQLList(typeFromAST(node.type)); + } + if (node.kind === Kind.NON_NULL_TYPE) { + return new GraphQLNonNull(typeFromAST(node.type)); } + return namedTypeFromAST(node); } - return fieldConfigMap; - } - function buildArgumentMap( - args: Maybe>, - ): GraphQLFieldConfigArgumentMap { - const argsNodes = args ?? []; + 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, + }); + } - 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, - }; + 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: typeFromAST(field.type), + description: field.description?.value, + args: buildArgumentMap(field.arguments), + deprecationReason: getDeprecationReason(field), + astNode: field, + }; + } + } + return fieldConfigMap; } - return argConfigMap; - } - function buildInputFieldMap( - nodes: ReadonlyArray< - InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode - >, - ): GraphQLInputFieldConfigMap { - const inputFieldMap = Object.create(null); - for (const node of nodes) { - const fieldsNodes = node.fields ?? []; + function buildArgumentMap( + args: Maybe>, + ): GraphQLFieldConfigArgumentMap { + const argsNodes = args ?? []; - for (const field of fieldsNodes) { + 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(field.type); + const type: any = typeFromAST(arg.type); - inputFieldMap[field.name.value] = { + argConfigMap[arg.name.value] = { type, - description: field.description?.value, - defaultValueLiteral: field.defaultValue, - deprecationReason: getDeprecationReason(field), - astNode: field, + description: arg.description?.value, + defaultValueLiteral: arg.defaultValue, + deprecationReason: getDeprecationReason(arg), + astNode: arg, }; } + return argConfigMap; } - 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 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 = typeFromAST(field.type); + + inputFieldMap[field.name.value] = { + type, + description: field.description?.value, + defaultValueLiteral: field.defaultValue, + deprecationReason: getDeprecationReason(field), + astNode: field, + }; + } } + return inputFieldMap; } - 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 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; + } - 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 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(namedTypeFromAST) ?? [], + ); + } - 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, - }); - } - 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), - }); + 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(namedTypeFromAST) ?? []); + } + + 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), + }); + } } } - } + }); } const stdTypeMap = new Map(