Skip to content

Commit

Permalink
feat(): Add sortSchema option
Browse files Browse the repository at this point in the history
  • Loading branch information
mattleff committed Jun 25, 2020
1 parent 6323f46 commit 4e8cbef
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 18 deletions.
54 changes: 36 additions & 18 deletions 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';
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -70,14 +85,17 @@ export class GraphQLSchemaBuilder {
resolvers: Function[],
autoSchemaFile: boolean | string,
options: BuildSchemaOptions = {},
sortSchema?: boolean,
): Promise<GraphQLSchema> {
const schema = await this.gqlSchemaFactory.create(resolvers, options);
if (typeof autoSchemaFile !== 'boolean') {
const filename = isString(autoSchemaFile)
? 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;
Expand Down
3 changes: 3 additions & 0 deletions lib/graphql.factory.ts
Expand Up @@ -6,6 +6,7 @@ import {
GraphQLObjectType,
GraphQLSchema,
GraphQLSchemaConfig,
lexicographicSortSchema,
printSchema,
} from 'graphql';
import {
Expand Down Expand Up @@ -92,6 +93,7 @@ export class GraphQLFactory {
}

schema = await transformSchema(schema);
schema = options.sortSchema ? lexicographicSortSchema(schema) : schema;
this.gqlSchemaHost.schema = schema;

return {
Expand Down Expand Up @@ -126,6 +128,7 @@ export class GraphQLFactory {

removeTempField(schema);
schema = await transformSchema(schema);
schema = options.sortSchema ? lexicographicSortSchema(schema) : schema;
this.gqlSchemaHost.schema = schema;

return {
Expand Down
4 changes: 4 additions & 0 deletions lib/interfaces/gql-module-options.interface.ts
Expand Up @@ -62,6 +62,10 @@ export interface GqlModuleOptions
* Enable/disable enhancers for @ResolveField()
*/
fieldResolverEnhancers?: Enhancer[];
/**
* Sort the schema lexicographically
*/
sortSchema?: boolean;
}

export interface GqlOptionsFactory {
Expand Down
48 changes: 48 additions & 0 deletions 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>(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();
});
});
59 changes: 59 additions & 0 deletions 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>(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
}
`;
16 changes: 16 additions & 0 deletions 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 {}
15 changes: 15 additions & 0 deletions 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 {}
88 changes: 88 additions & 0 deletions tests/utils/printed-schema.snapshot.ts
Expand Up @@ -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!
}
`;

0 comments on commit 4e8cbef

Please sign in to comment.