diff --git a/src/introspection/introspection.ts b/src/introspection/introspection.ts index 248452b6..4fb86ac0 100644 --- a/src/introspection/introspection.ts +++ b/src/introspection/introspection.ts @@ -1,14 +1,9 @@ import { - assertNamedType, getNamedType, getNullableType, - GraphQLDirective, - GraphQLEnumType, GraphQLFieldConfig, - GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, GraphQLInputFieldConfig, - GraphQLInputFieldConfigMap, GraphQLInputObjectType, GraphQLInterfaceType, GraphQLList, @@ -17,23 +12,16 @@ import { GraphQLObjectType, GraphQLOutputType, GraphQLSchema, - GraphQLType, - GraphQLUnionType, - isEnumType, isInputObjectType, isInterfaceType, - isIntrospectionType, - isListType, isNonNullType, isObjectType, - isScalarType, - isSpecifiedScalarType, - isUnionType, lexicographicSortSchema, } from 'graphql'; import { collectDirectlyReferencedTypes } from '../utils/collect-referenced-types'; -import { unreachable } from '../utils/unreachable'; +import { mapValues } from '../utils/mapValues'; +import { transformSchema } from '../utils/transformSchema'; declare module 'graphql' { interface GraphQLFieldExtensions<_TSource, _TContext, _TArgs> { @@ -282,174 +270,3 @@ export function getSchema( return transformSchema(schema, typeTransformers); } - -// FIXME: Contribute to graphql-js -export function transformSchema( - schema: GraphQLSchema, - transformType: ReadonlyArray< - (type: GraphQLNamedType) => GraphQLNamedType | null - >, - transformDirective: ReadonlyArray< - (directive: GraphQLDirective) => GraphQLDirective - > = [], -): GraphQLSchema { - const schemaConfig = schema.toConfig(); - - const typeMap = new Map(); - for (const oldType of schemaConfig.types) { - const newType = transformNamedType(oldType); - if (newType != null) { - typeMap.set(newType.name, newType); - } - } - - const directives = []; - for (const oldDirective of schemaConfig.directives) { - let newDirective = oldDirective; - for (const fn of transformDirective) { - if (newDirective === null) { - continue; - } - newDirective = fn(newDirective); - } - - if (newDirective == null) { - continue; - } - directives.push(replaceDirective(newDirective)); - } - - return new GraphQLSchema({ - ...schemaConfig, - types: Array.from(typeMap.values()), - directives, - query: replaceMaybeType(schemaConfig.query), - mutation: replaceMaybeType(schemaConfig.mutation), - subscription: replaceMaybeType(schemaConfig.subscription), - }); - - function replaceType(type: T): T { - if (isListType(type)) { - // @ts-expect-error Type mismatch - return new GraphQLList(replaceType(type.ofType)); - } else if (isNonNullType(type)) { - // @ts-expect-error Type mismatch - return new GraphQLNonNull(replaceType(type.ofType)); - } - // @ts-expect-error Type mismatch - return replaceNamedType(type); - } - - function replaceMaybeType( - maybeType: T | null | undefined, - ): T | null | undefined { - return maybeType && (replaceNamedType(maybeType) as T); - } - - function replaceTypes( - array: ReadonlyArray, - ): Array { - return array.map(replaceNamedType) as Array; - } - - function replaceNamedType(type: GraphQLNamedType): GraphQLNamedType { - return assertNamedType(typeMap.get(type.name)); - } - - function replaceDirective(directive: GraphQLDirective) { - const config = directive.toConfig(); - return new GraphQLDirective({ - ...config, - args: transformArgs(config.args), - }); - } - - function transformArgs(args: GraphQLFieldConfigArgumentMap) { - return mapValues(args, (arg) => ({ - ...arg, - type: replaceType(arg.type), - })); - } - - function transformFields(fieldsMap: GraphQLFieldConfigMap) { - return mapValues(fieldsMap, (field) => ({ - ...field, - type: replaceType(field.type), - args: field.args && transformArgs(field.args), - })); - } - - function transformInputFields(fieldsMap: GraphQLInputFieldConfigMap) { - return mapValues(fieldsMap, (field) => ({ - ...field, - type: replaceType(field.type), - })); - } - - function transformNamedType( - oldType: GraphQLNamedType, - ): GraphQLNamedType | null { - if (isIntrospectionType(oldType) || isSpecifiedScalarType(oldType)) { - return oldType; - } - - let newType = oldType; - for (const fn of transformType) { - const resultType = fn(newType); - if (resultType === null) { - return null; - } - newType = resultType; - } - - if (isScalarType(newType)) { - return newType; - } - if (isObjectType(newType)) { - const config = newType.toConfig(); - return new GraphQLObjectType({ - ...config, - interfaces: () => replaceTypes(config.interfaces), - fields: () => transformFields(config.fields), - }); - } - if (isInterfaceType(newType)) { - const config = newType.toConfig(); - return new GraphQLInterfaceType({ - ...config, - interfaces: () => replaceTypes(config.interfaces), - fields: () => transformFields(config.fields), - }); - } - if (isUnionType(newType)) { - const config = newType.toConfig(); - return new GraphQLUnionType({ - ...config, - types: () => replaceTypes(config.types), - }); - } - if (isEnumType(newType)) { - const config = newType.toConfig(); - return new GraphQLEnumType({ ...config }); - } - if (isInputObjectType(newType)) { - const config = newType.toConfig(); - return new GraphQLInputObjectType({ - ...config, - fields: () => transformInputFields(config.fields), - }); - } - unreachable(newType); - } -} - -function mapValues( - obj: { [key: string]: T }, - mapper: (value: T, key: string) => R | null, -): { [key: string]: R } { - return Object.fromEntries( - Object.entries(obj) - .map(([key, value]) => [key, mapper(value, key)]) - .filter(([, value]) => value != null), - ); -} diff --git a/src/utils/mapValues.ts b/src/utils/mapValues.ts new file mode 100644 index 00000000..ee0c5e38 --- /dev/null +++ b/src/utils/mapValues.ts @@ -0,0 +1,10 @@ +export function mapValues( + obj: { [key: string]: T }, + mapper: (value: T, key: string) => R | null, +): { [key: string]: R } { + return Object.fromEntries( + Object.entries(obj) + .map(([key, value]) => [key, mapper(value, key)]) + .filter(([, value]) => value != null), + ); +} diff --git a/src/utils/transformSchema.ts b/src/utils/transformSchema.ts new file mode 100644 index 00000000..bc83a767 --- /dev/null +++ b/src/utils/transformSchema.ts @@ -0,0 +1,200 @@ +import { + assertNamedType, + GraphQLDirective, + GraphQLEnumType, + GraphQLFieldConfigArgumentMap, + GraphQLFieldConfigMap, + GraphQLInputFieldConfigMap, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLList, + GraphQLNamedType, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLType, + GraphQLUnionType, + isEnumType, + isInputObjectType, + isInterfaceType, + isIntrospectionType, + isListType, + isNonNullType, + isObjectType, + isScalarType, + isSpecifiedScalarType, + isUnionType, +} from 'graphql/type'; + +import { unreachable } from '../utils/unreachable'; + +// FIXME: Contribute to graphql-js +export function transformSchema( + schema: GraphQLSchema, + transformType: ReadonlyArray< + (type: GraphQLNamedType) => GraphQLNamedType | null + >, + transformDirective: ReadonlyArray< + (directive: GraphQLDirective) => GraphQLDirective + > = [], +): GraphQLSchema { + const schemaConfig = schema.toConfig(); + + const typeMap = new Map(); + for (const oldType of schemaConfig.types) { + const newType = transformNamedType(oldType); + if (newType != null) { + typeMap.set(newType.name, newType); + } + } + + const directives = []; + for (const oldDirective of schemaConfig.directives) { + let newDirective = oldDirective; + for (const fn of transformDirective) { + if (newDirective === null) { + continue; + } + newDirective = fn(newDirective); + } + + if (newDirective == null) { + continue; + } + directives.push(replaceDirective(newDirective)); + } + + return new GraphQLSchema({ + ...schemaConfig, + types: Array.from(typeMap.values()), + directives, + query: replaceMaybeType(schemaConfig.query), + mutation: replaceMaybeType(schemaConfig.mutation), + subscription: replaceMaybeType(schemaConfig.subscription), + }); + + function replaceType(type: T): T { + if (isListType(type)) { + // @ts-expect-error Type mismatch + return new GraphQLList(replaceType(type.ofType)); + } else if (isNonNullType(type)) { + // @ts-expect-error Type mismatch + return new GraphQLNonNull(replaceType(type.ofType)); + } + // @ts-expect-error Type mismatch + return replaceNamedType(type); + } + + function replaceMaybeType( + maybeType: T | null | undefined, + ): T | null | undefined { + return maybeType && (replaceNamedType(maybeType) as T); + } + + function replaceTypes( + array: ReadonlyArray, + ): Array { + return array.map(replaceNamedType) as Array; + } + + function replaceNamedType(type: GraphQLNamedType): GraphQLNamedType { + return assertNamedType(typeMap.get(type.name)); + } + + function replaceDirective(directive: GraphQLDirective) { + const config = directive.toConfig(); + return new GraphQLDirective({ + ...config, + args: transformArgs(config.args), + }); + } + + function transformArgs(args: GraphQLFieldConfigArgumentMap) { + return mapValues(args, (arg) => ({ + ...arg, + type: replaceType(arg.type), + })); + } + + function transformFields(fieldsMap: GraphQLFieldConfigMap) { + return mapValues(fieldsMap, (field) => ({ + ...field, + type: replaceType(field.type), + args: field.args && transformArgs(field.args), + })); + } + + function transformInputFields(fieldsMap: GraphQLInputFieldConfigMap) { + return mapValues(fieldsMap, (field) => ({ + ...field, + type: replaceType(field.type), + })); + } + + function transformNamedType( + oldType: GraphQLNamedType, + ): GraphQLNamedType | null { + if (isIntrospectionType(oldType) || isSpecifiedScalarType(oldType)) { + return oldType; + } + + let newType = oldType; + for (const fn of transformType) { + const resultType = fn(newType); + if (resultType === null) { + return null; + } + newType = resultType; + } + + if (isScalarType(newType)) { + return newType; + } + if (isObjectType(newType)) { + const config = newType.toConfig(); + return new GraphQLObjectType({ + ...config, + interfaces: () => replaceTypes(config.interfaces), + fields: () => transformFields(config.fields), + }); + } + if (isInterfaceType(newType)) { + const config = newType.toConfig(); + return new GraphQLInterfaceType({ + ...config, + interfaces: () => replaceTypes(config.interfaces), + fields: () => transformFields(config.fields), + }); + } + if (isUnionType(newType)) { + const config = newType.toConfig(); + return new GraphQLUnionType({ + ...config, + types: () => replaceTypes(config.types), + }); + } + if (isEnumType(newType)) { + const config = newType.toConfig(); + return new GraphQLEnumType({ ...config }); + } + if (isInputObjectType(newType)) { + const config = newType.toConfig(); + return new GraphQLInputObjectType({ + ...config, + fields: () => transformInputFields(config.fields), + }); + } + unreachable(newType); + } +} + +function mapValues( + obj: { [key: string]: T }, + mapper: (value: T, key: string) => R | null, +): { [key: string]: R } { + return Object.fromEntries( + Object.entries(obj) + .map(([key, value]) => [key, mapper(value, key)]) + .filter(([, value]) => value != null), + ); +}