From 107be30275fc585b0b2cb7cbae9283b165270687 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Tue, 28 Jan 2020 23:50:24 +0800 Subject: [PATCH] schemaPrinter: preserve order of types Fixes #2362 It's a step toward having reversable `buildSchema`: ``` sdl === printSchema(buildSchema(sdl)) ``` At the same time if you need fully predictable SDL output (e.g. to diff schemas) you can achieve this using `llexicographicSortSchema`: ``` printSchema(lexicographicSortSchema(schema)) ``` But if some reason you can't use `lexicographicSortSchema` please open an issue and describe your use case in more details. --- src/type/__tests__/schema-test.js | 10 +- src/utilities/__tests__/extendSchema-test.js | 60 +-- src/utilities/__tests__/schemaPrinter-test.js | 376 +++++++++--------- src/utilities/extendSchema.js | 8 +- src/utilities/schemaPrinter.js | 5 +- 5 files changed, 228 insertions(+), 231 deletions(-) diff --git a/src/type/__tests__/schema-test.js b/src/type/__tests__/schema-test.js index ec98c576dd..570ff2bd1e 100644 --- a/src/type/__tests__/schema-test.js +++ b/src/type/__tests__/schema-test.js @@ -92,6 +92,11 @@ describe('Type System: Schema', () => { }); expect(printSchema(schema)).to.equal(dedent` + type Query { + article(id: String): Article + feed: [Article] + } + type Article { id: String isPublished: Boolean @@ -117,11 +122,6 @@ describe('Type System: Schema', () => { writeArticle: Article } - type Query { - article(id: String): Article - feed: [Article] - } - type Subscription { articleSubscribe(id: String): Article } diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index cb329abaff..a7403cd1f7 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -666,15 +666,15 @@ describe('extendSchema', () => { expect(validateSchema(extendedSchema)).to.deep.equal([]); expect(printSchemaChanges(schema, extendedSchema)).to.equal(dedent` + type SomeObject { + newField(arg1: String, arg2: NewInputObj!): String + } + input NewInputObj { field1: Int field2: [Float] field3: String! } - - type SomeObject { - newField(arg1: String, arg2: NewInputObj!): String - } `); }); @@ -754,8 +754,7 @@ describe('extendSchema', () => { scalar NewScalar - union NewUnion = NewObject - `; + union NewUnion = NewObject`; const extendAST = parse(` ${newTypesSDL} extend type SomeObject { @@ -771,7 +770,6 @@ describe('extendSchema', () => { expect(validateSchema(extendedSchema)).to.deep.equal([]); expect(printSchemaChanges(schema, extendedSchema)).to.equal(dedent` - ${newTypesSDL} type SomeObject { oldField: String newObject: NewObject @@ -781,6 +779,8 @@ describe('extendSchema', () => { newEnum: NewEnum newTree: [SomeObject]! } + + ${newTypesSDL} `); }); @@ -811,12 +811,12 @@ describe('extendSchema', () => { expect(validateSchema(extendedSchema)).to.deep.equal([]); expect(printSchemaChanges(schema, extendedSchema)).to.equal(dedent` - interface NewInterface { + type SomeObject implements OldInterface & NewInterface { + oldField: String newField: String } - type SomeObject implements OldInterface & NewInterface { - oldField: String + interface NewInterface { newField: String } `); @@ -864,8 +864,7 @@ describe('extendSchema', () => { type NewObject { foo: String - } - `; + }`; const extendAST = parse(` ${newTypesSDL} extend type SomeObject implements NewInterface { @@ -900,26 +899,27 @@ describe('extendSchema', () => { expect(validateSchema(extendedSchema)).to.deep.equal([]); expect(printSchemaChanges(schema, extendedSchema)).to.equal(dedent` - ${newTypesSDL} + type SomeObject implements SomeInterface & NewInterface & AnotherNewInterface { + oldField: String + newField: String + anotherNewField: String + } + enum SomeEnum { OLD_VALUE NEW_VALUE ANOTHER_NEW_VALUE } - input SomeInput { - oldField: String - newField: String - anotherNewField: String - } + union SomeUnion = SomeObject | NewObject | AnotherNewObject - type SomeObject implements SomeInterface & NewInterface & AnotherNewInterface { + input SomeInput { oldField: String newField: String anotherNewField: String } - union SomeUnion = SomeObject | NewObject | AnotherNewObject + ${newTypesSDL} `); }); @@ -958,12 +958,12 @@ describe('extendSchema', () => { expect(validateSchema(extendedSchema)).to.deep.equal([]); expect(printSchemaChanges(schema, extendedSchema)).to.equal(dedent` - interface AnotherInterface implements SomeInterface { + interface SomeInterface { oldField: String newField: String } - interface SomeInterface { + interface AnotherInterface implements SomeInterface { oldField: String newField: String } @@ -1015,12 +1015,12 @@ describe('extendSchema', () => { newField: String } - interface NewInterface { + type SomeObject implements SomeInterface & AnotherInterface & NewInterface { + oldField: String newField: String } - type SomeObject implements SomeInterface & AnotherInterface & NewInterface { - oldField: String + interface NewInterface { newField: String } `); @@ -1120,16 +1120,16 @@ describe('extendSchema', () => { expect(extendedSchema).to.not.equal(mutationSchema); expect(printSchema(mutationSchema)).to.equal(originalPrint); expect(printSchema(extendedSchema)).to.equal(dedent` - type Mutation { - mutationField: String - newMutationField: Int - } - type Query { queryField: String newQueryField: Int } + type Mutation { + mutationField: String + newMutationField: Int + } + type Subscription { subscriptionField: String newSubscriptionField: Int diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 93ac1143cd..81c3feb2e7 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -354,13 +354,13 @@ describe('Type System Printer', () => { int: Int } - interface Baz { - int: Int - } - interface Foo { str: String } + + interface Baz { + int: Int + } `); }); @@ -404,12 +404,12 @@ describe('Type System Printer', () => { int: Int } - interface Baz implements Foo { - int: Int + interface Foo { str: String } - interface Foo { + interface Baz implements Foo { + int: Int str: String } @@ -447,9 +447,7 @@ describe('Type System Printer', () => { const Schema = new GraphQLSchema({ types: [SingleUnion, MultipleUnion] }); const output = printForTest(Schema); expect(output).to.equal(dedent` - type Bar { - str: String - } + union SingleUnion = Foo type Foo { bool: Boolean @@ -457,7 +455,9 @@ describe('Type System Printer', () => { union MultipleUnion = Foo | Bar - union SingleUnion = Foo + type Bar { + str: String + } `); }); @@ -622,6 +622,116 @@ describe('Type System Printer', () => { reason: String = "No longer supported" ) on FIELD_DEFINITION | ENUM_VALUE + """ + A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. + """ + type __Schema { + """A list of all types supported by this server.""" + types: [__Type!]! + + """The type that query operations will be rooted at.""" + queryType: __Type! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type + + """A list of all directives supported by this server.""" + directives: [__Directive!]! + } + + """ + The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \`__TypeKind\` enum. + + Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. + """ + type __Type { + kind: __TypeKind! + name: String + description: String + fields(includeDeprecated: Boolean = false): [__Field!] + interfaces: [__Type!] + possibleTypes: [__Type!] + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + inputFields: [__InputValue!] + ofType: __Type + } + + """An enum describing what kind of type a given \`__Type\` is.""" + enum __TypeKind { + """Indicates this type is a scalar.""" + SCALAR + + """ + Indicates this type is an object. \`fields\` and \`interfaces\` are valid fields. + """ + OBJECT + + """ + Indicates this type is an interface. \`fields\`, \`interfaces\`, and \`possibleTypes\` are valid fields. + """ + INTERFACE + + """Indicates this type is a union. \`possibleTypes\` is a valid field.""" + UNION + + """Indicates this type is an enum. \`enumValues\` is a valid field.""" + ENUM + + """ + Indicates this type is an input object. \`inputFields\` is a valid field. + """ + INPUT_OBJECT + + """Indicates this type is a list. \`ofType\` is a valid field.""" + LIST + + """Indicates this type is a non-null. \`ofType\` is a valid field.""" + NON_NULL + } + + """ + Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. + """ + type __Field { + name: String! + description: String + args: [__InputValue!]! + type: __Type! + isDeprecated: Boolean! + deprecationReason: String + } + + """ + Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. + """ + type __InputValue { + name: String! + description: String + type: __Type! + + """ + A GraphQL-formatted string representing the default value for this input value. + """ + defaultValue: String + } + + """ + One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. + """ + type __EnumValue { + name: String! + description: String + isDeprecated: Boolean! + deprecationReason: String + } + """ A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. @@ -695,72 +805,55 @@ describe('Type System Printer', () => { """Location adjacent to an input object field definition.""" INPUT_FIELD_DEFINITION } + `; + expect(output).to.equal(introspectionSchema); + }); - """ - One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. - """ - type __EnumValue { - name: String! - description: String - isDeprecated: Boolean! - deprecationReason: String - } - - """ - Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. - """ - type __Field { - name: String! - description: String - args: [__InputValue!]! - type: __Type! - isDeprecated: Boolean! - deprecationReason: String - } + it('Print Introspection Schema with comment descriptions', () => { + const Schema = new GraphQLSchema({}); + const output = printIntrospectionSchema(Schema, { + commentDescriptions: true, + }); + const introspectionSchema = dedent` + # Directs the executor to include this field or fragment only when the \`if\` argument is true. + directive @include( + # Included when true. + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - """ - Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. - """ - type __InputValue { - name: String! - description: String - type: __Type! + # Directs the executor to skip this field or fragment when the \`if\` argument is true. + directive @skip( + # Skipped when true. + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - """ - A GraphQL-formatted string representing the default value for this input value. - """ - defaultValue: String - } + # Marks an element of a GraphQL schema as no longer supported. + directive @deprecated( + # Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/). + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE - """ - A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. - """ + # A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. type __Schema { - """A list of all types supported by this server.""" + # A list of all types supported by this server. types: [__Type!]! - """The type that query operations will be rooted at.""" + # The type that query operations will be rooted at. queryType: __Type! - """ - If this server supports mutation, the type that mutation operations will be rooted at. - """ + # If this server supports mutation, the type that mutation operations will be rooted at. mutationType: __Type - """ - If this server support subscription, the type that subscription operations will be rooted at. - """ + # If this server support subscription, the type that subscription operations will be rooted at. subscriptionType: __Type - """A list of all directives supported by this server.""" + # A list of all directives supported by this server. directives: [__Directive!]! } - """ - The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \`__TypeKind\` enum. - - Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. - """ + # The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \`__TypeKind\` enum. + # + # Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. type __Type { kind: __TypeKind! name: String @@ -773,65 +866,60 @@ describe('Type System Printer', () => { ofType: __Type } - """An enum describing what kind of type a given \`__Type\` is.""" + # An enum describing what kind of type a given \`__Type\` is. enum __TypeKind { - """Indicates this type is a scalar.""" + # Indicates this type is a scalar. SCALAR - """ - Indicates this type is an object. \`fields\` and \`interfaces\` are valid fields. - """ + # Indicates this type is an object. \`fields\` and \`interfaces\` are valid fields. OBJECT - """ - Indicates this type is an interface. \`fields\`, \`interfaces\`, and \`possibleTypes\` are valid fields. - """ + # Indicates this type is an interface. \`fields\`, \`interfaces\`, and \`possibleTypes\` are valid fields. INTERFACE - """Indicates this type is a union. \`possibleTypes\` is a valid field.""" + # Indicates this type is a union. \`possibleTypes\` is a valid field. UNION - """Indicates this type is an enum. \`enumValues\` is a valid field.""" + # Indicates this type is an enum. \`enumValues\` is a valid field. ENUM - """ - Indicates this type is an input object. \`inputFields\` is a valid field. - """ + # Indicates this type is an input object. \`inputFields\` is a valid field. INPUT_OBJECT - """Indicates this type is a list. \`ofType\` is a valid field.""" + # Indicates this type is a list. \`ofType\` is a valid field. LIST - """Indicates this type is a non-null. \`ofType\` is a valid field.""" + # Indicates this type is a non-null. \`ofType\` is a valid field. NON_NULL } - `; - expect(output).to.equal(introspectionSchema); - }); - it('Print Introspection Schema with comment descriptions', () => { - const Schema = new GraphQLSchema({}); - const output = printIntrospectionSchema(Schema, { - commentDescriptions: true, - }); - const introspectionSchema = dedent` - # Directs the executor to include this field or fragment only when the \`if\` argument is true. - directive @include( - # Included when true. - if: Boolean! - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + # Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. + type __Field { + name: String! + description: String + args: [__InputValue!]! + type: __Type! + isDeprecated: Boolean! + deprecationReason: String + } - # Directs the executor to skip this field or fragment when the \`if\` argument is true. - directive @skip( - # Skipped when true. - if: Boolean! - ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + # Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. + type __InputValue { + name: String! + description: String + type: __Type! - # Marks an element of a GraphQL schema as no longer supported. - directive @deprecated( - # Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/). - reason: String = "No longer supported" - ) on FIELD_DEFINITION | ENUM_VALUE + # A GraphQL-formatted string representing the default value for this input value. + defaultValue: String + } + + # One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. + type __EnumValue { + name: String! + description: String + isDeprecated: Boolean! + deprecationReason: String + } # A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. # @@ -902,94 +990,6 @@ describe('Type System Printer', () => { # Location adjacent to an input object field definition. INPUT_FIELD_DEFINITION } - - # One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. - type __EnumValue { - name: String! - description: String - isDeprecated: Boolean! - deprecationReason: String - } - - # Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. - type __Field { - name: String! - description: String - args: [__InputValue!]! - type: __Type! - isDeprecated: Boolean! - deprecationReason: String - } - - # Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. - type __InputValue { - name: String! - description: String - type: __Type! - - # A GraphQL-formatted string representing the default value for this input value. - defaultValue: String - } - - # A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. - type __Schema { - # A list of all types supported by this server. - types: [__Type!]! - - # The type that query operations will be rooted at. - queryType: __Type! - - # If this server supports mutation, the type that mutation operations will be rooted at. - mutationType: __Type - - # If this server support subscription, the type that subscription operations will be rooted at. - subscriptionType: __Type - - # A list of all directives supported by this server. - directives: [__Directive!]! - } - - # The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \`__TypeKind\` enum. - # - # Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. - type __Type { - kind: __TypeKind! - name: String - description: String - fields(includeDeprecated: Boolean = false): [__Field!] - interfaces: [__Type!] - possibleTypes: [__Type!] - enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - inputFields: [__InputValue!] - ofType: __Type - } - - # An enum describing what kind of type a given \`__Type\` is. - enum __TypeKind { - # Indicates this type is a scalar. - SCALAR - - # Indicates this type is an object. \`fields\` and \`interfaces\` are valid fields. - OBJECT - - # Indicates this type is an interface. \`fields\`, \`interfaces\`, and \`possibleTypes\` are valid fields. - INTERFACE - - # Indicates this type is a union. \`possibleTypes\` is a valid field. - UNION - - # Indicates this type is an enum. \`enumValues\` is a valid field. - ENUM - - # Indicates this type is an input object. \`inputFields\` is a valid field. - INPUT_OBJECT - - # Indicates this type is a list. \`ofType\` is a valid field. - LIST - - # Indicates this type is a non-null. \`ofType\` is a valid field. - NON_NULL - } `; expect(output).to.equal(introspectionSchema); }); diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 618af75cb4..69ac1f84ee 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -197,15 +197,15 @@ export function extendSchemaImpl( } const typeMap = Object.create(null); + for (const existingType of schemaConfig.types) { + typeMap[existingType.name] = extendNamedType(existingType); + } + for (const typeNode of typeDefs) { const name = typeNode.name.value; typeMap[name] = stdTypeMap[name] || buildType(typeNode); } - for (const existingType of schemaConfig.types) { - typeMap[existingType.name] = extendNamedType(existingType); - } - const operationTypes = { // Get the extended root operation types. query: schemaConfig.query && replaceNamedType(schemaConfig.query), diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index 4edcc19dbb..6e26d4653d 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -85,10 +85,7 @@ function printFilteredSchema( options, ): string { const directives = schema.getDirectives().filter(directiveFilter); - const typeMap = schema.getTypeMap(); - const types = objectValues(typeMap) - .sort((type1, type2) => type1.name.localeCompare(type2.name)) - .filter(typeFilter); + const types = objectValues(schema.getTypeMap()).filter(typeFilter); return ( [printSchemaDefinition(schema)]