Repository: MichalLytek/type-graphql
Pull request #465 (2019-11-17 18:18:58)

Sort emitted schema SDL file
This PR aims to solve issues #379, #442 and #417.

However because the `lexicographicSortSchema` utility strips off all additional properties from types config, the schema sorting only applies to the emitted schema SDL file for now, on the contrary to @mattiassundling fork: https://github.com/mattiassundling/type-graphql/commit/5a6fbc6e4aeb97a3dd78a3d2a09982109b69ce58

So if you need a sorted schema in GraphQL Playground or other tools that uses introspection and if you don't rely on the putting metadata in the type config, you should use `lexicographicSortSchema` on your own, e.g.:

```ts
transformSchema: (schema: GraphQLSchema) => lexicographicSortSchema(schema)
```

codecov[bot]: # [Codecov](https://codecov.io/gh/MichalLytek/type-graphql/pull/465?src=pr&el=h1) Report
> Merging [#465](https://codecov.io/gh/MichalLytek/type-graphql/pull/465?src=pr&el=desc) into [master](https://codecov.io/gh/MichalLytek/type-graphql/commit/20c81f4f7ee82779595002d25784fce3a8ff8b9b?src=pr&el=desc) will **increase** coverage by `0.02%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/MichalLytek/type-graphql/pull/465/graphs/tree.svg?width=650&token=Z6D63dJD2e&height=150&src=pr)](https://codecov.io/gh/MichalLytek/type-graphql/pull/465?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #465      +/-   ##
==========================================
+ Coverage   94.74%   94.77%   +0.02%     
==========================================
  Files          74       74              
  Lines        1238     1243       +5     
  Branches      234      235       +1     
==========================================
+ Hits         1173     1178       +5     
  Misses         62       62              
  Partials        3        3
```


| [Impacted Files](https://codecov.io/gh/MichalLytek/type-graphql/pull/465?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/utils/buildSchema.ts](https://codecov.io/gh/MichalLytek/type-graphql/pull/465/diff?src=pr&el=tree#diff-c3JjL3V0aWxzL2J1aWxkU2NoZW1hLnRz) | `100% <100%> (ø)` | :arrow_up: |
| [src/utils/emitSchemaDefinitionFile.ts](https://codecov.io/gh/MichalLytek/type-graphql/pull/465/diff?src=pr&el=tree#diff-c3JjL3V0aWxzL2VtaXRTY2hlbWFEZWZpbml0aW9uRmlsZS50cw==) | `100% <100%> (ø)` | :arrow_up: |
| [src/utils/index.ts](https://codecov.io/gh/MichalLytek/type-graphql/pull/465/diff?src=pr&el=tree#diff-c3JjL3V0aWxzL2luZGV4LnRz) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/MichalLytek/type-graphql/pull/465?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/MichalLytek/type-graphql/pull/465?src=pr&el=footer). Last update [20c81f4...9599e7f](https://codecov.io/gh/MichalLytek/type-graphql/pull/465?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eaf8a71a..a93cab4a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ ### Features
 - **Breaking Change**: update `graphql-js` peer dependency to `^14.5.8`
 - **Breaking Change**: update `graphql-query-complexity` dependency to `^0.4.1` and drop support for `fieldConfigEstimator` (use `fieldExtensionsEstimator` instead)
 - **Breaking Change**: remove deprecated `DepreciationOptions` interface
+- **Breaking Change**: introduce `sortedSchema` option in `PrintSchemaOptions` and emit sorted schema file by default
 - update `TypeResolver` interface to match with `GraphQLTypeResolver` from `graphql-js`
 - add basic support for directives with `@Directive()` decorator (#369)
 ### Fixes
diff --git a/docs/emit-schema.md b/docs/emit-schema.md
index 42a089dd..f718a40f 100644
--- a/docs/emit-schema.md
+++ b/docs/emit-schema.md
@@ -17,6 +17,7 @@
   emitSchemaFile: {
     path: __dirname + "/schema.gql",
     commentDescriptions: true,
+    sortedSchema: false, // by default the printed schema is sorted alphabetically
   },
 });
 ```
diff --git a/examples/generic-types/schema.gql b/examples/generic-types/schema.gql
index 0a56b53e..527ba055 100644
--- a/examples/generic-types/schema.gql
+++ b/examples/generic-types/schema.gql
@@ -12,13 +12,13 @@ type Query {
 }
 
 type Recipe {
-  title: String!
   description: String!
   ratings: [Int!]!
+  title: String!
 }
 
 type RecipesResponse {
+  hasMore: Boolean!
   items: [Recipe!]!
   total: Int!
-  hasMore: Boolean!
 }
diff --git a/examples/interfaces-inheritance/schema.gql b/examples/interfaces-inheritance/schema.gql
index 41cb220e..5b7e641c 100644
--- a/examples/interfaces-inheritance/schema.gql
+++ b/examples/interfaces-inheritance/schema.gql
@@ -9,33 +9,33 @@ The javascript `Date` as string. Type represents date and time as the ISO Date s
 scalar DateTime
 
 type Employee implements IPerson {
-  id: ID!
-  name: String!
   age: Int!
   companyName: String!
+  id: ID!
+  name: String!
 }
 
 input EmployeeInput {
-  name: String!
-  dateOfBirth: DateTime!
   companyName: String!
+  dateOfBirth: DateTime!
+  name: String!
 }
 
 interface IPerson {
+  age: Int!
   id: ID!
   name: String!
-  age: Int!
 }
 
 type Mutation {
-  addStudent(input: StudentInput!): Student!
   addEmployee(input: EmployeeInput!): Employee!
+  addStudent(input: StudentInput!): Student!
 }
 
 type Person implements IPerson {
+  age: Int!
   id: ID!
   name: String!
-  age: Int!
 }
 
 type Query {
@@ -43,14 +43,14 @@ type Query {
 }
 
 type Student implements IPerson {
+  age: Int!
   id: ID!
   name: String!
-  age: Int!
   universityName: String!
 }
 
 input StudentInput {
-  name: String!
   dateOfBirth: DateTime!
+  name: String!
   universityName: String!
 }
diff --git a/examples/mixin-classes/schema.gql b/examples/mixin-classes/schema.gql
index 6dbcf0ff..114e2ce0 100644
--- a/examples/mixin-classes/schema.gql
+++ b/examples/mixin-classes/schema.gql
@@ -4,19 +4,19 @@
 # -----------------------------------------------
 
 input AmendUserInput {
-  forename: String!
-  surname: String
   dateOfBirth: DateTime!
   email: String!
+  forename: String!
   id: Int!
+  surname: String
 }
 
 input CreateUserInput {
-  forename: String!
-  surname: String
   dateOfBirth: DateTime!
   email: String!
+  forename: String!
   password: String!
+  surname: String
 }
 
 """
@@ -25,8 +25,8 @@ The javascript `Date` as string. Type represents date and time as the ISO Date s
 scalar DateTime
 
 type Mutation {
-  createUser(input: CreateUserInput!): User!
   amendUser(input: AmendUserInput!): User!
+  createUser(input: CreateUserInput!): User!
 }
 
 type Query {
@@ -34,9 +34,9 @@ type Query {
 }
 
 type User {
-  forename: String!
-  surname: String
   dateOfBirth: DateTime!
   email: String!
+  forename: String!
   id: Int!
+  surname: String
 }
diff --git a/examples/simple-usage/schema.gql b/examples/simple-usage/schema.gql
index 1a2564a2..f2d492a2 100644
--- a/examples/simple-usage/schema.gql
+++ b/examples/simple-usage/schema.gql
@@ -3,7 +3,9 @@
 # !!!   DO NOT MODIFY THIS FILE BY YOURSELF   !!!
 # -----------------------------------------------
 
-# The javascript `Date` as string. Type represents date and time as the ISO Date string.
+"""
+The javascript `Date` as string. Type represents date and time as the ISO Date string.
+"""
 scalar DateTime
 
 type Mutation {
@@ -13,24 +15,24 @@ type Mutation {
 type Query {
   recipe(title: String!): Recipe
 
-  # Get all the recipes from around the world 
+  """Get all the recipes from around the world """
   recipes: [Recipe!]!
 }
 
-# Object representing cooking recipe
+"""Object representing cooking recipe"""
 type Recipe {
-  title: String!
-  specification: String @deprecated(reason: "Use `description` field instead")
+  averageRating: Float
+  creationDate: DateTime!
 
-  # The recipe description with preparation info
+  """The recipe description with preparation info"""
   description: String
   ratings: [Int!]!
-  creationDate: DateTime!
   ratingsCount(minRate: Int = 0): Int!
-  averageRating: Float
+  specification: String @deprecated(reason: "Use `description` field instead")
+  title: String!
 }
 
 input RecipeInput {
-  title: String!
   description: String
+  title: String!
 }
diff --git a/examples/typegoose/schema.gql b/examples/typegoose/schema.gql
index 1b4e46dd..25561bb1 100644
--- a/examples/typegoose/schema.gql
+++ b/examples/typegoose/schema.gql
@@ -22,9 +22,9 @@ type Query {
 }
 
 type Rate {
-  value: Int!
   date: DateTime!
   user: User!
+  value: Int!
 }
 
 input RateInput {
@@ -34,15 +34,15 @@ input RateInput {
 
 type Recipe {
   _id: ObjectId!
-  title: String!
+  author: User!
   description: String
   ratings: [Rate!]!
-  author: User!
+  title: String!
 }
 
 input RecipeInput {
-  title: String!
   description: String
+  title: String!
 }
 
 type User {
diff --git a/src/utils/buildSchema.ts b/src/utils/buildSchema.ts
index 00bcf5c0..b0f3b987 100644
--- a/src/utils/buildSchema.ts
+++ b/src/utils/buildSchema.ts
@@ -1,5 +1,4 @@
 import { GraphQLSchema } from "graphql";
-import { Options as PrintSchemaOptions } from "graphql/utilities/schemaPrinter";
 import * as path from "path";
 
 import { SchemaGenerator, SchemaGeneratorOptions } from "../schema/schema-generator";
@@ -8,10 +7,11 @@ import {
   emitSchemaDefinitionFileSync,
   emitSchemaDefinitionFile,
   defaultPrintSchemaOptions,
+  PrintSchemaOptions,
 } from "./emitSchemaDefinitionFile";
 import { NonEmptyArray } from "./types";
 
-interface EmitSchemaFileOptions extends PrintSchemaOptions {
+interface EmitSchemaFileOptions extends Partial<PrintSchemaOptions> {
   path?: string;
 }
 
@@ -25,6 +25,7 @@ export interface BuildSchemaOptions extends Omit<SchemaGeneratorOptions, "resolv
    */
   emitSchemaFile?: string | boolean | EmitSchemaFileOptions;
 }
+
 export async function buildSchema(options: BuildSchemaOptions): Promise<GraphQLSchema> {
   const resolvers = loadResolvers(options);
   const schema = await SchemaGenerator.generateFromMetadata({ ...options, resolvers });
@@ -77,7 +78,7 @@ function getEmitSchemaDefinitionFileOptions(
         : defaultSchemaFilePath,
     printSchemaOptions:
       typeof buildSchemaOptions.emitSchemaFile === "object"
-        ? buildSchemaOptions.emitSchemaFile
+        ? { ...defaultPrintSchemaOptions, ...buildSchemaOptions.emitSchemaFile }
         : defaultPrintSchemaOptions,
   };
 }
diff --git a/src/utils/emitSchemaDefinitionFile.ts b/src/utils/emitSchemaDefinitionFile.ts
index 3c9da592..3a362756 100644
--- a/src/utils/emitSchemaDefinitionFile.ts
+++ b/src/utils/emitSchemaDefinitionFile.ts
@@ -1,9 +1,16 @@
-import { GraphQLSchema, printSchema } from "graphql";
-import { Options as PrintSchemaOptions } from "graphql/utilities/schemaPrinter";
+import { GraphQLSchema, printSchema, lexicographicSortSchema } from "graphql";
+import { Options as GraphQLPrintSchemaOptions } from "graphql/utilities/schemaPrinter";
 
 import { outputFile, outputFileSync } from "../helpers/filesystem";
 
-export const defaultPrintSchemaOptions: PrintSchemaOptions = { commentDescriptions: false };
+export interface PrintSchemaOptions extends Required<GraphQLPrintSchemaOptions> {
+  sortedSchema: boolean;
+}
+
+export const defaultPrintSchemaOptions: PrintSchemaOptions = {
+  commentDescriptions: false,
+  sortedSchema: true,
+};
 
 const generatedSchemaWarning = /* graphql */ `\
 # -----------------------------------------------
@@ -18,7 +25,7 @@ export function emitSchemaDefinitionFileSync(
   schema: GraphQLSchema,
   options: PrintSchemaOptions = defaultPrintSchemaOptions,
 ) {
-  const schemaFileContent = generatedSchemaWarning + printSchema(schema, options);
+  const schemaFileContent = getSchemaFileContent(schema, options);
   outputFileSync(schemaFilePath, schemaFileContent);
 }
 
@@ -27,6 +34,11 @@ export async function emitSchemaDefinitionFile(
   schema: GraphQLSchema,
   options: PrintSchemaOptions = defaultPrintSchemaOptions,
 ) {
-  const schemaFileContent = generatedSchemaWarning + printSchema(schema, options);
+  const schemaFileContent = getSchemaFileContent(schema, options);
   await outputFile(schemaFilePath, schemaFileContent);
 }
+
+function getSchemaFileContent(schema: GraphQLSchema, options: PrintSchemaOptions) {
+  const schemaToEmit = options.sortedSchema ? lexicographicSortSchema(schema) : schema;
+  return generatedSchemaWarning + printSchema(schemaToEmit, options);
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 1323ae62..ae0474d9 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,4 +1,9 @@
 export { buildSchema, buildSchemaSync, BuildSchemaOptions } from "./buildSchema";
 export { buildTypeDefsAndResolvers } from "./buildTypeDefsAndResolvers";
-export { emitSchemaDefinitionFile, emitSchemaDefinitionFileSync } from "./emitSchemaDefinitionFile";
+export {
+  emitSchemaDefinitionFile,
+  emitSchemaDefinitionFileSync,
+  PrintSchemaOptions,
+  defaultPrintSchemaOptions,
+} from "./emitSchemaDefinitionFile";
 export { ContainerType, ContainerGetter } from "./container";


Test patch:
diff --git a/tests/functional/emit-schema-sdl.ts b/tests/functional/emit-schema-sdl.ts
index 140d8838..1f429fa1 100644
--- a/tests/functional/emit-schema-sdl.ts
+++ b/tests/functional/emit-schema-sdl.ts
@@ -13,6 +13,8 @@ import {
   ObjectType,
   Query,
   Resolver,
+  PrintSchemaOptions,
+  defaultPrintSchemaOptions,
 } from "../../src";
 import * as filesystem from "../../src/helpers/filesystem";
 
@@ -52,13 +54,21 @@ describe("Emitting schema definition file", () => {
     rimraf(TEST_DIR, done);
   });
 
-  function checkSchemaSDL(contents: string, commentPrefix = `"""`) {
-    expect(contents).toContain("THIS FILE WAS GENERATED");
-    expect(contents).toContain("MyObject");
-    if (commentPrefix === `"""`) {
-      expect(contents).toContain(`"""Description test"""`);
+  function checkSchemaSDL(
+    SDL: string,
+    { commentDescriptions, sortedSchema }: PrintSchemaOptions = defaultPrintSchemaOptions,
+  ) {
+    expect(SDL).toContain("THIS FILE WAS GENERATED");
+    expect(SDL).toContain("MyObject");
+    if (sortedSchema) {
+      expect(SDL.indexOf("descriptionProperty")).toBeLessThan(SDL.indexOf("normalProperty"));
     } else {
-      expect(contents).toContain(`# Description test`);
+      expect(SDL.indexOf("descriptionProperty")).toBeGreaterThan(SDL.indexOf("normalProperty"));
+    }
+    if (commentDescriptions) {
+      expect(SDL).toContain(`# Description test`);
+    } else {
+      expect(SDL).toContain(`"""Description test"""`);
     }
   }
 
@@ -70,6 +80,17 @@ describe("Emitting schema definition file", () => {
       checkSchemaSDL(fs.readFileSync(targetPath).toString());
     });
 
+    it("should use provided options to write file with schema SDL", async () => {
+      const targetPath = path.join(TEST_DIR, "schemas", "test1", "schema.gql");
+      const options: PrintSchemaOptions = {
+        commentDescriptions: true,
+        sortedSchema: false,
+      };
+      await emitSchemaDefinitionFile(targetPath, schema, options);
+      expect(fs.existsSync(targetPath)).toEqual(true);
+      checkSchemaSDL(fs.readFileSync(targetPath).toString(), options);
+    });
+
     it("should throw error when unknown error occur while writing file with schema SDL", async () => {
       jest.spyOn(filesystem, "fsWriteFile").mockRejectedValueOnce({ code: "TEST ERROR" });
       const targetPath = path.join(TEST_DIR, "schemas", "fail1", "schema.gql");
@@ -105,6 +126,17 @@ describe("Emitting schema definition file", () => {
       checkSchemaSDL(fs.readFileSync(targetPath).toString());
     });
 
+    it("should use provided options to write file with schema SDL", async () => {
+      const targetPath = path.join(TEST_DIR, "schemas", "test1", "schema.gql");
+      const options: PrintSchemaOptions = {
+        commentDescriptions: true,
+        sortedSchema: false,
+      };
+      emitSchemaDefinitionFileSync(targetPath, schema, options);
+      expect(fs.existsSync(targetPath)).toEqual(true);
+      checkSchemaSDL(fs.readFileSync(targetPath).toString(), options);
+    });
+
     it("should throw error when unknown error occur while writing file with schema SDL", () => {
       jest.spyOn(fs, "writeFileSync").mockImplementationOnce(() => {
         throw { code: "TEST ERROR" };
@@ -165,13 +197,17 @@ describe("Emitting schema definition file", () => {
         emitSchemaFile: {
           commentDescriptions: true,
           path: targetPath,
+          sortedSchema: false,
         },
       });
       expect(fs.existsSync(targetPath)).toEqual(true);
-      checkSchemaSDL(fs.readFileSync(targetPath).toString(), "#");
+      checkSchemaSDL(fs.readFileSync(targetPath).toString(), {
+        commentDescriptions: true,
+        sortedSchema: false,
+      });
     });
 
-    it("should read EmitSchemaFileOptions and set default path", async () => {
+    it("should read EmitSchemaFileOptions and set default path and sorting schema", async () => {
       jest.spyOn(process, "cwd").mockImplementation(() => TEST_DIR);
       const targetPath = path.join(process.cwd(), "schema.gql");
       await buildSchema({
@@ -181,7 +217,10 @@ describe("Emitting schema definition file", () => {
         },
       });
       expect(fs.existsSync(targetPath)).toEqual(true);
-      checkSchemaSDL(fs.readFileSync(targetPath).toString(), "#");
+      checkSchemaSDL(fs.readFileSync(targetPath).toString(), {
+        ...defaultPrintSchemaOptions,
+        commentDescriptions: true,
+      });
     });
   });
 
@@ -214,13 +253,17 @@ describe("Emitting schema definition file", () => {
         emitSchemaFile: {
           commentDescriptions: true,
           path: targetPath,
+          sortedSchema: false,
         },
       });
       expect(fs.existsSync(targetPath)).toEqual(true);
-      checkSchemaSDL(fs.readFileSync(targetPath).toString(), "#");
+      checkSchemaSDL(fs.readFileSync(targetPath).toString(), {
+        commentDescriptions: true,
+        sortedSchema: false,
+      });
     });
 
-    it("should read EmitSchemaFileOptions and set default path", async () => {
+    it("should read EmitSchemaFileOptions and set default path and sorting schema", async () => {
       jest.spyOn(process, "cwd").mockImplementation(() => TEST_DIR);
       const targetPath = path.join(process.cwd(), "schema.gql");
       buildSchemaSync({
@@ -230,7 +273,10 @@ describe("Emitting schema definition file", () => {
         },
       });
       expect(fs.existsSync(targetPath)).toEqual(true);
-      checkSchemaSDL(fs.readFileSync(targetPath).toString(), "#");
+      checkSchemaSDL(fs.readFileSync(targetPath).toString(), {
+        ...defaultPrintSchemaOptions,
+        commentDescriptions: true,
+      });
     });
   });
 });