diff --git a/src/index.js b/src/index.js index 8c2b97af8f..e63fea5805 100644 --- a/src/index.js +++ b/src/index.js @@ -326,6 +326,8 @@ export { // Extends an existing GraphQLSchema from a parsed GraphQL Schema // language AST. extendSchema, + // Sort a GraphQLSchema. + lexographicSortSchema, // Print a GraphQLSchema to GraphQL Schema language. printSchema, // Prints the built-in introspection schema in the Schema Language diff --git a/src/utilities/__tests__/lexographicSortSchema-test.js b/src/utilities/__tests__/lexographicSortSchema-test.js new file mode 100644 index 0000000000..50a775ac90 --- /dev/null +++ b/src/utilities/__tests__/lexographicSortSchema-test.js @@ -0,0 +1,364 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import dedent from '../../jsutils/dedent'; +import { printSchema } from '../schemaPrinter'; +import { buildSchema } from '../buildASTSchema'; +import { lexographicSortSchema } from '../lexographicSortSchema'; + +function sortSDL(sdl) { + const schema = buildSchema(sdl); + return printSchema(lexographicSortSchema(schema)); +} + +describe('lexographicSortSchema', () => { + it('sort fields', () => { + const sorted = sortSDL(dedent` + input Bar { + barB: String + barA: String + barC: String + } + + interface FooInterface { + fooB: String + fooA: String + fooC: String + } + + type FooType implements FooInterface { + fooC: String + fooA: String + fooB: String + } + + type Query { + dummy(arg: Bar): FooType + } + `); + + expect(sorted).to.equal(dedent` + input Bar { + barA: String + barB: String + barC: String + } + + interface FooInterface { + fooA: String + fooB: String + fooC: String + } + + type FooType implements FooInterface { + fooA: String + fooB: String + fooC: String + } + + type Query { + dummy(arg: Bar): FooType + } + `); + }); + + it('sort implemented interfaces', () => { + const sorted = sortSDL(dedent` + interface FooA { + dummy: String + } + + interface FooB { + dummy: String + } + + interface FooC { + dummy: String + } + + type Query implements FooB & FooA & FooC { + dummy: String + } + `); + + expect(sorted).to.equal(dedent` + interface FooA { + dummy: String + } + + interface FooB { + dummy: String + } + + interface FooC { + dummy: String + } + + type Query implements FooA & FooB & FooC { + dummy: String + } + `); + }); + + it('sort types in union', () => { + const sorted = sortSDL(dedent` + type FooA { + dummy: String + } + + type FooB { + dummy: String + } + + type FooC { + dummy: String + } + + union FooUnion = FooB | FooA | FooC + + type Query { + dummy: FooUnion + } + `); + + expect(sorted).to.equal(dedent` + type FooA { + dummy: String + } + + type FooB { + dummy: String + } + + type FooC { + dummy: String + } + + union FooUnion = FooA | FooB | FooC + + type Query { + dummy: FooUnion + } + `); + }); + + it('sort enum values', () => { + const sorted = sortSDL(dedent` + enum Foo { + B + C + A + } + + type Query { + dummy: Foo + } + `); + + expect(sorted).to.equal(dedent` + enum Foo { + A + B + C + } + + type Query { + dummy: Foo + } + `); + }); + + it('sort field arguments', () => { + const sorted = sortSDL(dedent` + type Query { + dummy(argB: Int, argA: String, argC: Float): ID + } + `); + + expect(sorted).to.equal(dedent` + type Query { + dummy(argA: String, argB: Int, argC: Float): ID + } + `); + }); + + it('sort types', () => { + const sorted = sortSDL(dedent` + type Query { + dummy(arg1: FooF, arg2: FooA, arg3: FooG): FooD + } + + type FooC implements FooE { + dummy: String + } + + enum FooG { + enumValue + } + + scalar FooA + + input FooF { + dummy: String + } + + union FooD = FooC | FooB + + interface FooE { + dummy: String + } + + type FooB { + dummy: String + } + `); + + expect(sorted).to.equal(dedent` + scalar FooA + + type FooB { + dummy: String + } + + type FooC implements FooE { + dummy: String + } + + union FooD = FooB | FooC + + interface FooE { + dummy: String + } + + input FooF { + dummy: String + } + + enum FooG { + enumValue + } + + type Query { + dummy(arg1: FooF, arg2: FooA, arg3: FooG): FooD + } + `); + }); + + it('sort directive arguments', () => { + const sorted = sortSDL(dedent` + directive @test(argC: Float, argA: String, argB: Int) on FIELD + + type Query { + dummy: String + } + `); + + expect(sorted).to.equal(dedent` + directive @test(argA: String, argB: Int, argC: Float) on FIELD + + type Query { + dummy: String + } + `); + }); + + it('sort directive locations', () => { + const sorted = sortSDL(dedent` + directive @test(argC: Float, argA: String, argB: Int) on UNION | FIELD | ENUM + + type Query { + dummy: String + } + `); + + expect(sorted).to.equal(dedent` + directive @test(argA: String, argB: Int, argC: Float) on ENUM | FIELD | UNION + + type Query { + dummy: String + } + `); + }); + + it('sort directives', () => { + const sorted = sortSDL(dedent` + directive @fooC on FIELD + + directive @fooB on UNION + + directive @fooA on ENUM + + type Query { + dummy: String + } + `); + + expect(sorted).to.equal(dedent` + directive @fooA on ENUM + + directive @fooB on UNION + + directive @fooC on FIELD + + type Query { + dummy: String + } + `); + }); + + it('sort recursive types', () => { + const sorted = sortSDL(dedent` + interface FooC { + fooB: FooB + fooA: FooA + fooC: FooC + } + + type FooB implements FooC { + fooB: FooB + fooA: FooA + } + + type FooA implements FooC { + fooB: FooB + fooA: FooA + } + + type Query { + fooC: FooC + fooB: FooB + fooA: FooA + } + `); + + expect(sorted).to.equal(dedent` + type FooA implements FooC { + fooA: FooA + fooB: FooB + } + + type FooB implements FooC { + fooA: FooA + fooB: FooB + } + + interface FooC { + fooA: FooA + fooB: FooB + fooC: FooC + } + + type Query { + fooA: FooA + fooB: FooB + fooC: FooC + } + `); + }); +}); diff --git a/src/utilities/index.js b/src/utilities/index.js index 90d0795fe4..be2a6dcf96 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -53,6 +53,9 @@ export { buildASTSchema, buildSchema, getDescription } from './buildASTSchema'; // Extends an existing GraphQLSchema from a parsed GraphQL Schema language AST. export { extendSchema } from './extendSchema'; +// Sort a GraphQLSchema. +export { lexographicSortSchema } from './lexographicSortSchema'; + // Print a GraphQLSchema to GraphQL Schema language. export { printSchema, diff --git a/src/utilities/lexographicSortSchema.js b/src/utilities/lexographicSortSchema.js new file mode 100644 index 0000000000..0e6abc48a3 --- /dev/null +++ b/src/utilities/lexographicSortSchema.js @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { ObjMap } from '../jsutils/ObjMap'; +import keyValMap from '../jsutils/keyValMap'; +import objectValues from '../jsutils/objectValues'; +import { GraphQLSchema } from '../type/schema'; +import { GraphQLDirective } from '../type/directives'; +import { GraphQLList, GraphQLNonNull } from '../type/wrappers'; +import type { GraphQLNamedType } from '../type/definition'; +import { + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, + isListType, + isNonNullType, + isScalarType, + isObjectType, + isInterfaceType, + isUnionType, + isEnumType, + isInputObjectType, +} from '../type/definition'; +import { isSpecifiedScalarType } from '../type/scalars'; +import { isIntrospectionType } from '../type/introspection'; + +/** + * Sort GraphQLSchema. + */ +export function lexographicSortSchema(schema: GraphQLSchema): GraphQLSchema { + const cache = Object.create(null); + + const sortMaybeType = maybeType => maybeType && sortNamedType(maybeType); + return new GraphQLSchema({ + types: sortByName(objectValues(schema.getTypeMap()).map(sortNamedType)), + directives: sortByName(schema.getDirectives()).map(sortDirective), + query: sortMaybeType(schema.getQueryType()), + mutation: sortMaybeType(schema.getMutationType()), + subscription: sortMaybeType(schema.getSubscriptionType()), + astNode: schema.astNode, + }); + + function sortDirective(directive) { + return new GraphQLDirective({ + name: directive.name, + description: directive.description, + locations: sortBy(directive.locations, x => x), + args: sortArgs(directive.args), + astNode: directive.astNode, + }); + } + + function sortArgs(args) { + return keyValMap( + sortByName(args), + arg => arg.name, + arg => ({ + ...arg, + type: sortType(arg.type), + }), + ); + } + + function sortFields(fieldsMap) { + return () => + sortObjMap(fieldsMap, field => ({ + type: sortType(field.type), + args: sortArgs(field.args), + resolve: field.resolve, + subscribe: field.subscribe, + deprecationReason: field.deprecationReason, + description: field.description, + astNode: field.astNode, + })); + } + + function sortInputFields(fieldsMap) { + return () => + sortObjMap(fieldsMap, field => ({ + type: sortType(field.type), + defaultValue: field.defaultValue, + description: field.description, + astNode: field.astNode, + })); + } + + function sortType(type) { + if (isListType(type)) { + return new GraphQLList(sortType(type.ofType)); + } else if (isNonNullType(type)) { + return new GraphQLNonNull(sortType(type.ofType)); + } + return sortNamedType(type); + } + + function sortTypes(arr: Array): () => Array { + return () => sortByName(arr).map(sortNamedType); + } + + function sortNamedType(type: T): T { + if (isSpecifiedScalarType(type) || isIntrospectionType(type)) { + return type; + } + + let sortedType = cache[type.name]; + if (!sortedType) { + sortedType = sortNamedTypeImpl(type); + cache[type.name] = sortedType; + } + return ((sortedType: any): T); + } + + function sortNamedTypeImpl(type) { + if (isScalarType(type)) { + return type; + } else if (isObjectType(type)) { + return new GraphQLObjectType({ + name: type.name, + interfaces: sortTypes(type.getInterfaces()), + fields: sortFields(type.getFields()), + isTypeOf: type.isTypeOf, + description: type.description, + astNode: type.astNode, + extensionASTNodes: type.extensionASTNodes, + }); + } else if (isInterfaceType(type)) { + return new GraphQLInterfaceType({ + name: type.name, + fields: sortFields(type.getFields()), + resolveType: type.resolveType, + description: type.description, + astNode: type.astNode, + extensionASTNodes: type.extensionASTNodes, + }); + } else if (isUnionType(type)) { + return new GraphQLUnionType({ + name: type.name, + types: sortTypes(type.getTypes()), + resolveType: type.resolveType, + description: type.description, + astNode: type.astNode, + }); + } else if (isEnumType(type)) { + return new GraphQLEnumType({ + name: type.name, + values: keyValMap( + sortByName(type.getValues()), + val => val.name, + val => ({ + value: val.value, + deprecationReason: val.deprecationReason, + description: val.description, + astNode: val.astNode, + }), + ), + description: type.description, + astNode: type.astNode, + }); + } else if (isInputObjectType(type)) { + return new GraphQLInputObjectType({ + name: type.name, + fields: sortInputFields(type.getFields()), + description: type.description, + astNode: type.astNode, + }); + } + throw new Error(`Unknown type: "${type}"`); + } +} + +function sortObjMap(map: ObjMap, sortValueFn?: T => R): ObjMap { + const sortedMap = Object.create(null); + const sortedKeys = sortBy(Object.keys(map), x => x); + for (const key of sortedKeys) { + const value = map[key]; + sortedMap[key] = sortValueFn ? sortValueFn(value) : value; + } + return sortedMap; +} + +function sortByName(array: $ReadOnlyArray): Array { + return sortBy(array, obj => obj.name); +} + +function sortBy(array: $ReadOnlyArray, mapToKey: T => string): Array { + return array.slice().sort((obj1, obj2) => { + const key1 = mapToKey(obj1); + const key2 = mapToKey(obj2); + return key1.localeCompare(key2); + }); +}