From 4e8cbef08b05fd7fbd9326523f48b34f70b3aeaf Mon Sep 17 00:00:00 2001 From: Matthew Leffler Date: Thu, 25 Jun 2020 15:17:59 -0600 Subject: [PATCH] feat(): Add sortSchema option --- lib/graphql-schema.builder.ts | 54 ++++++++---- lib/graphql.factory.ts | 3 + .../gql-module-options.interface.ts | 4 + tests/e2e/graphql-sort-auto-schema.spec.ts | 48 ++++++++++ tests/e2e/graphql-sort-schema.spec.ts | 59 +++++++++++++ tests/graphql/sort-auto-schema.module.ts | 16 ++++ tests/graphql/sort-schema.module.ts | 15 ++++ tests/utils/printed-schema.snapshot.ts | 88 +++++++++++++++++++ 8 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 tests/e2e/graphql-sort-auto-schema.spec.ts create mode 100644 tests/e2e/graphql-sort-schema.spec.ts create mode 100644 tests/graphql/sort-auto-schema.module.ts create mode 100644 tests/graphql/sort-schema.module.ts diff --git a/lib/graphql-schema.builder.ts b/lib/graphql-schema.builder.ts index fa846b09f..44f5e99a9 100644 --- a/lib/graphql-schema.builder.ts +++ b/lib/graphql-schema.builder.ts @@ -1,7 +1,12 @@ import { Injectable } from '@nestjs/common'; import { loadPackage } from '@nestjs/common/utils/load-package.util'; import { isString } from '@nestjs/common/utils/shared.utils'; -import { GraphQLSchema, printSchema, specifiedDirectives } from 'graphql'; +import { + GraphQLSchema, + printSchema, + specifiedDirectives, + lexicographicSortSchema, +} from 'graphql'; import { resolve } from 'path'; import { GRAPHQL_SDL_FILE_HEADER } from './graphql.constants'; import { GqlModuleOptions } from './interfaces'; @@ -26,11 +31,16 @@ export class GraphQLSchemaBuilder { const scalarsMap = this.scalarsExplorerService.getScalarsMap(); try { const buildSchemaOptions = options.buildSchemaOptions || {}; - return await this.buildSchema(resolvers, autoSchemaFile, { - ...buildSchemaOptions, - scalarsMap, - schemaDirectives: options.schemaDirectives, - }); + return await this.buildSchema( + resolvers, + autoSchemaFile, + { + ...buildSchemaOptions, + scalarsMap, + schemaDirectives: options.schemaDirectives, + }, + options.sortSchema, + ); } catch (err) { if (err && err.details) { console.error(err.details); @@ -47,17 +57,22 @@ export class GraphQLSchemaBuilder { const scalarsMap = this.scalarsExplorerService.getScalarsMap(); try { const buildSchemaOptions = options.buildSchemaOptions || {}; - return await this.buildSchema(resolvers, autoSchemaFile, { - ...buildSchemaOptions, - directives: [ - ...specifiedDirectives, - ...this.loadFederationDirectives(), - ...((buildSchemaOptions && buildSchemaOptions.directives) || []), - ], - scalarsMap, - schemaDirectives: options.schemaDirectives, - skipCheck: true, - }); + return await this.buildSchema( + resolvers, + autoSchemaFile, + { + ...buildSchemaOptions, + directives: [ + ...specifiedDirectives, + ...this.loadFederationDirectives(), + ...((buildSchemaOptions && buildSchemaOptions.directives) || []), + ], + scalarsMap, + schemaDirectives: options.schemaDirectives, + skipCheck: true, + }, + options.sortSchema, + ); } catch (err) { if (err && err.details) { console.error(err.details); @@ -70,6 +85,7 @@ export class GraphQLSchemaBuilder { resolvers: Function[], autoSchemaFile: boolean | string, options: BuildSchemaOptions = {}, + sortSchema?: boolean, ): Promise { const schema = await this.gqlSchemaFactory.create(resolvers, options); if (typeof autoSchemaFile !== 'boolean') { @@ -77,7 +93,9 @@ export class GraphQLSchemaBuilder { ? autoSchemaFile : resolve(process.cwd(), 'schema.gql'); - const fileContent = GRAPHQL_SDL_FILE_HEADER + printSchema(schema); + const fileContent = + GRAPHQL_SDL_FILE_HEADER + + printSchema(sortSchema ? lexicographicSortSchema(schema) : schema); await this.fileSystemHelper.writeFile(filename, fileContent); } return schema; diff --git a/lib/graphql.factory.ts b/lib/graphql.factory.ts index d55d5a4e1..0f5508550 100644 --- a/lib/graphql.factory.ts +++ b/lib/graphql.factory.ts @@ -6,6 +6,7 @@ import { GraphQLObjectType, GraphQLSchema, GraphQLSchemaConfig, + lexicographicSortSchema, printSchema, } from 'graphql'; import { @@ -92,6 +93,7 @@ export class GraphQLFactory { } schema = await transformSchema(schema); + schema = options.sortSchema ? lexicographicSortSchema(schema) : schema; this.gqlSchemaHost.schema = schema; return { @@ -126,6 +128,7 @@ export class GraphQLFactory { removeTempField(schema); schema = await transformSchema(schema); + schema = options.sortSchema ? lexicographicSortSchema(schema) : schema; this.gqlSchemaHost.schema = schema; return { diff --git a/lib/interfaces/gql-module-options.interface.ts b/lib/interfaces/gql-module-options.interface.ts index 590a7400b..9864071f3 100644 --- a/lib/interfaces/gql-module-options.interface.ts +++ b/lib/interfaces/gql-module-options.interface.ts @@ -62,6 +62,10 @@ export interface GqlModuleOptions * Enable/disable enhancers for @ResolveField() */ fieldResolverEnhancers?: Enhancer[]; + /** + * Sort the schema lexicographically + */ + sortSchema?: boolean; } export interface GqlOptionsFactory { diff --git a/tests/e2e/graphql-sort-auto-schema.spec.ts b/tests/e2e/graphql-sort-auto-schema.spec.ts new file mode 100644 index 000000000..28f66076b --- /dev/null +++ b/tests/e2e/graphql-sort-auto-schema.spec.ts @@ -0,0 +1,48 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { printSchema, GraphQLSchema } from 'graphql'; +import { FileSystemHelper } from '../../lib/schema-builder/helpers/file-system.helper'; +import { sortedPrintedSchemaSnapshot } from '../utils/printed-schema.snapshot'; +import { GRAPHQL_SDL_FILE_HEADER } from '../../lib/graphql.constants'; +import { GraphQLSchemaHost } from '../../lib'; +import { SortAutoSchemaModule } from '../graphql/sort-auto-schema.module'; + +describe('GraphQL sort autoSchemaFile schema', () => { + let app: INestApplication; + let schema: GraphQLSchema; + let writeFileMock: jest.Mock; + + beforeEach(async () => { + writeFileMock = jest.fn().mockImplementation(() => Promise.resolve()); + const module = await Test.createTestingModule({ + imports: [SortAutoSchemaModule], + }) + .overrideProvider(FileSystemHelper) + .useValue({ + writeFile: writeFileMock, + }) + .compile(); + + app = module.createNestApplication(); + await app.init(); + + const graphQLSchemaHost = app.get(GraphQLSchemaHost); + schema = graphQLSchemaHost.schema; + }); + + it('should match schema snapshot', () => { + expect(GRAPHQL_SDL_FILE_HEADER + printSchema(schema)).toEqual( + sortedPrintedSchemaSnapshot, + ); + + expect(writeFileMock).toHaveBeenCalledTimes(1); + expect(writeFileMock).toHaveBeenCalledWith( + 'schema.graphql', + sortedPrintedSchemaSnapshot, + ); + }); + + afterEach(async () => { + await app.close(); + }); +}); diff --git a/tests/e2e/graphql-sort-schema.spec.ts b/tests/e2e/graphql-sort-schema.spec.ts new file mode 100644 index 000000000..1c0e3975f --- /dev/null +++ b/tests/e2e/graphql-sort-schema.spec.ts @@ -0,0 +1,59 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { printSchema, GraphQLSchema } from 'graphql'; +import { SortSchemaModule } from '../graphql/sort-schema.module'; +import { GRAPHQL_SDL_FILE_HEADER } from '../../lib/graphql.constants'; +import { GraphQLSchemaHost } from '../../lib'; + +describe('GraphQL sort schema', () => { + let app: INestApplication; + let schema: GraphQLSchema; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [SortSchemaModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + + const graphQLSchemaHost = app.get(GraphQLSchemaHost); + schema = graphQLSchemaHost.schema; + }); + + it('should match schema snapshot', () => { + expect(GRAPHQL_SDL_FILE_HEADER + printSchema(schema)).toEqual( + expectedSchema, + ); + }); + + afterEach(async () => { + await app.close(); + }); +}); + +const expectedSchema = `# ------------------------------------------------------ +# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) +# ------------------------------------------------------ + +type Cat { + age: Int + color: String + id: Int + name: String + weight: Int +} + +type Mutation { + createCat(name: String): Cat +} + +type Query { + cat(id: ID!): Cat + getCats: [Cat] +} + +type Subscription { + catCreated: Cat +} +`; diff --git a/tests/graphql/sort-auto-schema.module.ts b/tests/graphql/sort-auto-schema.module.ts new file mode 100644 index 000000000..5cf89e949 --- /dev/null +++ b/tests/graphql/sort-auto-schema.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { GraphQLModule } from '../../lib'; +import { RecipesModule } from '../code-first/recipes/recipes.module'; +import { DirectionsModule } from '../code-first/directions/directions.module'; + +@Module({ + imports: [ + RecipesModule, + DirectionsModule, + GraphQLModule.forRoot({ + autoSchemaFile: 'schema.graphql', + sortSchema: true, + }), + ], +}) +export class SortAutoSchemaModule {} diff --git a/tests/graphql/sort-schema.module.ts b/tests/graphql/sort-schema.module.ts new file mode 100644 index 000000000..e091145f9 --- /dev/null +++ b/tests/graphql/sort-schema.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { join } from 'path'; +import { GraphQLModule } from '../../lib'; +import { CatsModule } from './cats/cats.module'; + +@Module({ + imports: [ + CatsModule, + GraphQLModule.forRoot({ + typePaths: [join(__dirname, '**', '*.graphql')], + sortSchema: true, + }), + ], +}) +export class SortSchemaModule {} diff --git a/tests/utils/printed-schema.snapshot.ts b/tests/utils/printed-schema.snapshot.ts index f461f7b77..e18b33ae8 100644 --- a/tests/utils/printed-schema.snapshot.ts +++ b/tests/utils/printed-schema.snapshot.ts @@ -95,3 +95,91 @@ type Subscription { recipeAdded: Recipe! } `; + +export const sortedPrintedSchemaSnapshot = `# ------------------------------------------------------ +# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) +# ------------------------------------------------------ + +type Category { + description: String! + name: String! + tags: [String!]! +} + +"""Date custom scalar type""" +scalar Date + +"""The basic directions""" +enum Direction { + Down + Left + Right + Up +} + +type Ingredient { + id: ID! + + """ingredient name""" + name: String @deprecated(reason: "is deprecated") +} + +"""example interface""" +interface IRecipe { + id: ID! + title: String! +} + +type Mutation { + addRecipe(newRecipeData: NewRecipeInput!): Recipe! + removeRecipe(id: String!): Boolean! +} + +"""new recipe input""" +input NewRecipeInput { + description: String + ingredients: [String!]! + + """recipe title""" + title: String! +} + +type Query { + categories: [Category!]! + move(direction: Direction!): Direction! + + """get recipe by id""" + recipe( + """recipe id""" + id: String = "1" + ): IRecipe! + recipes( + """number of items to skip""" + skip: Int = 0 + take: Int = 25 + ): [Recipe!]! + search: [SearchResultUnion!]! @deprecated(reason: "test") +} + +"""recipe object type""" +type Recipe implements IRecipe { + averageRating: Float! + count(status: String, type: String): Float! + creationDate: Date! + description: String + id: ID! + ingredients: [Ingredient!]! + lastRate: Float + rating: Float! + tags: [String!]! + title: String! +} + +"""Search result description""" +union SearchResultUnion = Ingredient | Recipe + +type Subscription { + """subscription description""" + recipeAdded: Recipe! +} +`;