Skip to content

Commit

Permalink
Merge pull request #415 from MichalLytek/omit-not-used-types
Browse files Browse the repository at this point in the history
Omit types not used by loaded resolvers
  • Loading branch information
MichalLytek authored Sep 7, 2019
2 parents 0c8cd5d + 26a54af commit edf4706
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 41 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased
<!-- here goes all the unreleased changes descriptions -->
### Features
- **Breaking Change**: emit in schema only types actually used by provided resolvers classes (#415)

### Fixes
- refactor union types function syntax handling to prevent possible errors with circular refs

Expand Down
19 changes: 18 additions & 1 deletion docs/bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,22 @@ In the configuration object you must provide a `resolvers` property, which can b
import { FirstResolver, SecondResolver } from "../app/src/resolvers";
// ...
const schema = await buildSchema({
resolvers: [FirstResolver, SampleResolver],
resolvers: [FirstResolver, SecondResolver],
});
```

Be aware that only operations (queries, mutation, etc.) defined in the resolvers classes (and types directly connected to them) will be emitted in schema.

So if you e.g. have defined some object types that implements an interface but are not directly used in other types definition (like a part of an union, a type of a field or a return type of an operation), you need to provide them manually in `orphanedTypes` options of `buildSchema`:

```typescript
import { FirstResolver, SecondResolver } from "../app/src/resolvers";
import { FirstObject } from "../app/src/types";
// ...
const schema = await buildSchema({
resolvers: [FirstResolver, SecondResolver],
// here provide all the types that are missing in schema
orphanedTypes: [FirstObject],
});
```

Expand All @@ -28,6 +43,8 @@ const schema = await buildSchema({
});
```

> Be aware that in case of providing paths to resolvers files, TypeGraphQL will emit all the operations and types that are imported in the resolvers files or their dependencies.
There are also other options related to advanced features like [authorization](authorization.md) or [validation](validation.md) - you can read about them in docs.

To make `await` work, we need to declare it as an async function. Example of `main.ts` file:
Expand Down
6 changes: 6 additions & 0 deletions examples/interfaces-inheritance/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import "reflect-metadata";
import { ApolloServer } from "apollo-server";
import * as path from "path";
import { buildSchema } from "../../src";

import { MultiResolver } from "./resolver";
import { Person } from "./person/person.type";

async function bootstrap() {
// build TypeGraphQL executable schema
const schema = await buildSchema({
resolvers: [MultiResolver],
emitSchemaFile: path.resolve(__dirname, "schema.gql"),
// provide the type that implements an interface
// but is not directly used in schema
orphanedTypes: [Person],
});

// Create GraphQL server
Expand Down
56 changes: 56 additions & 0 deletions examples/interfaces-inheritance/schema.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# -----------------------------------------------
# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!!
# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!!
# -----------------------------------------------

"""
The javascript `Date` as string. Type represents date and time as the ISO Date string.
"""
scalar DateTime

type Employee implements IPerson {
id: ID!
name: String!
age: Int!
companyName: String!
}

input EmployeeInput {
name: String!
dateOfBirth: DateTime!
companyName: String!
}

interface IPerson {
id: ID!
name: String!
age: Int!
}

type Mutation {
addStudent(input: StudentInput!): Student!
addEmployee(input: EmployeeInput!): Employee!
}

type Person implements IPerson {
id: ID!
name: String!
age: Int!
}

type Query {
persons: [IPerson!]!
}

type Student implements IPerson {
id: ID!
name: String!
age: Int!
universityName: String!
}

input StudentInput {
name: String!
dateOfBirth: DateTime!
universityName: String!
}
80 changes: 62 additions & 18 deletions src/schema/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ interface UnionTypeInfo {
}

export interface SchemaGeneratorOptions extends BuildContextOptions {
/**
* Array of resolvers classes
*/
resolvers?: Function[];
/**
* Array of orphaned type classes that are not used explicitly in GraphQL types definitions
*/
orphanedTypes?: Function[];
/**
* Disable checking on build the correctness of a schema
*/
Expand Down Expand Up @@ -99,11 +107,12 @@ export abstract class SchemaGenerator {
getMetadataStorage().build();
this.buildTypesInfo();

const orphanedTypes = options.orphanedTypes || (options.resolvers ? [] : undefined);
const schema = new GraphQLSchema({
query: this.buildRootQueryType(),
mutation: this.buildRootMutationType(),
subscription: this.buildRootSubscriptionType(),
types: this.buildOtherTypes(),
query: this.buildRootQueryType(options.resolvers),
mutation: this.buildRootMutationType(options.resolvers),
subscription: this.buildRootSubscriptionType(options.resolvers),
types: this.buildOtherTypes(orphanedTypes),
});

BuildContext.reset();
Expand Down Expand Up @@ -374,40 +383,59 @@ export abstract class SchemaGenerator {
});
}

private static buildRootQueryType(): GraphQLObjectType {
private static buildRootQueryType(resolvers?: Function[]): GraphQLObjectType {
const queriesHandlers = this.filterHandlersByResolvers(getMetadataStorage().queries, resolvers);

return new GraphQLObjectType({
name: "Query",
fields: this.generateHandlerFields(getMetadataStorage().queries),
fields: this.generateHandlerFields(queriesHandlers),
});
}

private static buildRootMutationType(): GraphQLObjectType | undefined {
if (getMetadataStorage().mutations.length === 0) {
private static buildRootMutationType(resolvers?: Function[]): GraphQLObjectType | undefined {
const mutationsHandlers = this.filterHandlersByResolvers(
getMetadataStorage().mutations,
resolvers,
);
if (mutationsHandlers.length === 0) {
return;
}

return new GraphQLObjectType({
name: "Mutation",
fields: this.generateHandlerFields(getMetadataStorage().mutations),
fields: this.generateHandlerFields(mutationsHandlers),
});
}

private static buildRootSubscriptionType(): GraphQLObjectType | undefined {
if (getMetadataStorage().subscriptions.length === 0) {
private static buildRootSubscriptionType(resolvers?: Function[]): GraphQLObjectType | undefined {
const subscriptionsHandlers = this.filterHandlersByResolvers(
getMetadataStorage().subscriptions,
resolvers,
);
if (subscriptionsHandlers.length === 0) {
return;
}

return new GraphQLObjectType({
name: "Subscription",
fields: this.generateSubscriptionsFields(getMetadataStorage().subscriptions),
fields: this.generateSubscriptionsFields(subscriptionsHandlers),
});
}

private static buildOtherTypes(): GraphQLNamedType[] {
// TODO: investigate the need of directly providing this types
// maybe GraphQL can use only the types provided indirectly
private static buildOtherTypes(orphanedTypes?: Function[]): GraphQLNamedType[] {
return [
...this.objectTypesInfo.filter(it => !it.isAbstract).map(it => it.type),
...this.interfaceTypesInfo.filter(it => !it.isAbstract).map(it => it.type),
...this.inputTypesInfo.filter(it => !it.isAbstract).map(it => it.type),
...this.filterTypesInfoByIsAbstractAndOrphanedTypesAndExtractType(
this.objectTypesInfo,
orphanedTypes,
),
...this.filterTypesInfoByIsAbstractAndOrphanedTypesAndExtractType(
this.interfaceTypesInfo,
orphanedTypes,
),
...this.filterTypesInfoByIsAbstractAndOrphanedTypesAndExtractType(
this.inputTypesInfo,
orphanedTypes,
),
];
}

Expand Down Expand Up @@ -602,4 +630,20 @@ export abstract class SchemaGenerator {
return this.objectTypesInfo.find(objectType => objectType.target === resolvedType)!.type;
};
}

private static filterHandlersByResolvers<T extends ResolverMetadata>(
handlers: T[],
resolvers: Function[] | undefined,
) {
return resolvers ? handlers.filter(query => resolvers.includes(query.target)) : handlers;
}

private static filterTypesInfoByIsAbstractAndOrphanedTypesAndExtractType(
typesInfo: Array<ObjectTypeInfo | InterfaceTypeInfo | InputObjectTypeInfo>,
orphanedTypes: Function[] | undefined,
) {
return typesInfo
.filter(it => !it.isAbstract && (!orphanedTypes || orphanedTypes.includes(it.target)))
.map(it => it.type);
}
}
30 changes: 18 additions & 12 deletions src/utils/buildSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import {
emitSchemaDefinitionFile,
defaultPrintSchemaOptions,
} from "./emitSchemaDefinitionFile";
import { NonEmptyArray } from "./types";

interface EmitSchemaFileOptions extends PrintSchemaOptions {
path?: string;
}

export interface BuildSchemaOptions extends SchemaGeneratorOptions {
export interface BuildSchemaOptions extends Omit<SchemaGeneratorOptions, "resolvers"> {
/** Array of resolvers classes or glob paths to resolver files */
resolvers: Array<Function | string>;
resolvers: NonEmptyArray<Function> | NonEmptyArray<string>;
/**
* Path to the file to where emit the schema
* or config object with print schema options
Expand All @@ -25,8 +26,8 @@ export interface BuildSchemaOptions extends SchemaGeneratorOptions {
emitSchemaFile?: string | boolean | EmitSchemaFileOptions;
}
export async function buildSchema(options: BuildSchemaOptions): Promise<GraphQLSchema> {
loadResolvers(options);
const schema = await SchemaGenerator.generateFromMetadata(options);
const resolvers = loadResolvers(options);
const schema = await SchemaGenerator.generateFromMetadata({ ...options, resolvers });
if (options.emitSchemaFile) {
const { schemaFileName, printSchemaOptions } = getEmitSchemaDefinitionFileOptions(options);
await emitSchemaDefinitionFile(schemaFileName, schema, printSchemaOptions);
Expand All @@ -35,24 +36,29 @@ export async function buildSchema(options: BuildSchemaOptions): Promise<GraphQLS
}

export function buildSchemaSync(options: BuildSchemaOptions): GraphQLSchema {
loadResolvers(options);
const schema = SchemaGenerator.generateFromMetadataSync(options);
const resolvers = loadResolvers(options);
const schema = SchemaGenerator.generateFromMetadataSync({ ...options, resolvers });
if (options.emitSchemaFile) {
const { schemaFileName, printSchemaOptions } = getEmitSchemaDefinitionFileOptions(options);
emitSchemaDefinitionFileSync(schemaFileName, schema, printSchemaOptions);
}
return schema;
}

function loadResolvers(options: BuildSchemaOptions) {
function loadResolvers(options: BuildSchemaOptions): Function[] | undefined {
// TODO: remove that check as it's covered by `NonEmptyArray` type guard
if (options.resolvers.length === 0) {
throw new Error("Empty `resolvers` array property found in `buildSchema` options!");
}
options.resolvers.forEach(resolver => {
if (typeof resolver === "string") {
loadResolversFromGlob(resolver);
}
});
if (options.resolvers.some((resolver: Function | string) => typeof resolver === "string")) {
(options.resolvers as string[]).forEach(resolver => {
if (typeof resolver === "string") {
loadResolversFromGlob(resolver);
}
});
return undefined;
}
return options.resolvers as Function[];
}

function getEmitSchemaDefinitionFileOptions(
Expand Down
4 changes: 3 additions & 1 deletion src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// https:/sindresorhus/type-fest/blob/c6a3d8c2603e9d6b8f20edb0157faae15548cd5b/source/merge-exclusive.d.ts

export type Without<FirstType, SecondType> = {
[KeyType in Exclude<keyof FirstType, keyof SecondType>]?: never
[KeyType in Exclude<keyof FirstType, keyof SecondType>]?: never;
};

export type MergeExclusive<FirstType, SecondType> = (FirstType | SecondType) extends object
? (Without<FirstType, SecondType> & SecondType) | (Without<SecondType, FirstType> & FirstType)
: FirstType | SecondType;

export type NonEmptyArray<TItem> = [TItem, ...TItem[]];
2 changes: 2 additions & 0 deletions tests/functional/default-nullable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ describe("buildSchema -> nullableByDefault", () => {
beforeEach(async () => {
({ schemaIntrospection, queryType } = await getSchemaInfo({
resolvers: [SampleResolverClass],
orphanedTypes: [SampleObjectClass],
}));
sampleObjectType = schemaIntrospection.types.find(
type => type.name === "SampleObject",
Expand Down Expand Up @@ -121,6 +122,7 @@ describe("buildSchema -> nullableByDefault", () => {
beforeEach(async () => {
({ schemaIntrospection, queryType } = await getSchemaInfo({
resolvers: [SampleResolverClass],
orphanedTypes: [SampleObjectClass],
nullableByDefault: true,
}));
sampleObjectType = schemaIntrospection.types.find(
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/deprecation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ describe("Deprecation", () => {
@Resolver(of => SampleObject)
class SampleResolver {
@Query()
normalQuery(): string {
return "normalQuery";
normalQuery(): SampleObject {
return {} as SampleObject;
}

@Query({ deprecationReason: "sample query deprecation reason" })
Expand Down
1 change: 1 addition & 0 deletions tests/functional/description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ describe("Description", () => {
// get builded schema info from retrospection
const schemaInfo = await getSchemaInfo({
resolvers: [SampleResolver],
orphanedTypes: [SampleObject],
});
schemaIntrospection = schemaInfo.schemaIntrospection;
queryType = schemaInfo.queryType;
Expand Down
16 changes: 16 additions & 0 deletions tests/functional/interfaces-and-inheritance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ describe("Interfaces and inheritance", () => {
// get builded schema info from retrospection
const schemaInfo = await getSchemaInfo({
resolvers: [SampleResolver],
orphanedTypes: [
SampleInterface1,
SampleInterfaceExtending1,
SampleImplementingObject1,
SampleImplementingObject2,
SampleMultiImplementingObject,
SampleExtendingImplementingObject,
SampleExtendingObject2,
],
});
queryType = schemaInfo.queryType;
schemaIntrospection = schemaInfo.schemaIntrospection;
Expand Down Expand Up @@ -619,6 +628,13 @@ describe("Interfaces and inheritance", () => {

schema = await buildSchema({
resolvers: [InterfacesResolver],
orphanedTypes: [
FirstImplementation,
SecondInterfaceWithStringResolveTypeObject,
FirstInterfaceWithStringResolveTypeObject,
SecondInterfaceWithClassResolveTypeObject,
FirstInterfaceWithClassResolveTypeObject,
],
});
});

Expand Down
Loading

0 comments on commit edf4706

Please sign in to comment.