diff --git a/dev.js b/dev.js index f064da5c2..a4f7cb3dd 100644 --- a/dev.js +++ b/dev.js @@ -9,6 +9,7 @@ require("ts-node/register/transpile-only"); // require("./examples/query-complexity/index.ts"); // require("./examples/redis-subscriptions/index.ts"); // require("./examples/resolvers-inheritance/index.ts"); +require("./examples/multiple-schemas/index.ts"); // require("./examples/simple-subscriptions/index.ts"); // require("./examples/simple-usage/index.ts"); // require("./examples/typegoose/index.ts"); diff --git a/examples/multiple-schemas/index.ts b/examples/multiple-schemas/index.ts new file mode 100644 index 000000000..0b506bf5e --- /dev/null +++ b/examples/multiple-schemas/index.ts @@ -0,0 +1,41 @@ +import "reflect-metadata"; +import { ApolloServer } from "apollo-server"; +import { Container } from "typedi"; +import { buildSchema } from "../../src"; + +import { RecipeResolver } from "./recipe/recipe.resolver"; +import { Recipe } from "./recipe/recipe.type"; +import { PersonResolver } from "./person/person.resolver"; +import { Person } from "./person/person.type"; +import { GetAllArgs } from "./resource/resource.resolver"; +import * as path from "path"; + +async function bootstrap() { + // build TypeGraphQL executable schema + const schemaRecipe = await buildSchema({ + resolvers: [RecipeResolver], + types: [Recipe, GetAllArgs], + container: Container, + emitSchemaFile: path.resolve(__dirname, "schemaRecipe.gql"), + }); + + const schemaPerson = await buildSchema({ + resolvers: [PersonResolver], + types: [Person, GetAllArgs], + container: Container, + emitSchemaFile: path.resolve(__dirname, "schemaPerson.gql"), + }); + + // Create GraphQL server + const serverRecipe = new ApolloServer({ schema: schemaRecipe }); + const serverPerson = new ApolloServer({ schema: schemaPerson }); + + // Start the server + const { url: recipeUrl } = await serverRecipe.listen(4000); + console.log(`Server recipe is running, GraphQL Playground available at ${recipeUrl}`); + + const { url: personUrl } = await serverPerson.listen(4001); + console.log(`Server recipe is running, GraphQL Playground available at ${personUrl}`); +} + +bootstrap(); diff --git a/examples/multiple-schemas/person/person.resolver.ts b/examples/multiple-schemas/person/person.resolver.ts new file mode 100644 index 000000000..c05eeef4b --- /dev/null +++ b/examples/multiple-schemas/person/person.resolver.ts @@ -0,0 +1,42 @@ +import { Resolver, Arg, Int, Mutation } from "../../../src"; + +import { ResourceResolver } from "../resource/resource.resolver"; +import { Person } from "./person.type"; +import { PersonRole } from "./person.role"; + +const persons: Person[] = [ + { + id: 1, + name: "Person 1", + age: 23, + role: PersonRole.Normal, + }, + { + id: 2, + name: "Person 2", + age: 48, + role: PersonRole.Admin, + }, +]; + +@Resolver() +export class PersonResolver extends ResourceResolver(Person, persons) { + // here you can add resource-specific operations + + @Mutation() + promote(@Arg("personId", type => Int) personId: number): boolean { + // you have full access to base resolver class fields and methods + + const person = this.resourceService.getOne(personId); + if (!person) { + throw new Error("Person not found!"); + } + + if (person.role === PersonRole.Normal) { + person.role = PersonRole.Pro; + return true; + } + + return false; + } +} diff --git a/examples/multiple-schemas/person/person.role.ts b/examples/multiple-schemas/person/person.role.ts new file mode 100644 index 000000000..a3ee95ac3 --- /dev/null +++ b/examples/multiple-schemas/person/person.role.ts @@ -0,0 +1,9 @@ +import { registerEnumType } from "../../../src"; + +export enum PersonRole { + Normal, + Pro, + Admin, +} + +registerEnumType(PersonRole, { name: "PersonRole" }); diff --git a/examples/multiple-schemas/person/person.type.ts b/examples/multiple-schemas/person/person.type.ts new file mode 100644 index 000000000..0f5955ea1 --- /dev/null +++ b/examples/multiple-schemas/person/person.type.ts @@ -0,0 +1,19 @@ +import { ObjectType, Field, Int } from "../../../src"; + +import { Resource } from "../resource/resource"; +import { PersonRole } from "./person.role"; + +@ObjectType() +export class Person implements Resource { + @Field() + id: number; + + @Field() + name: string; + + @Field(type => Int) + age: number; + + @Field(type => PersonRole) + role: PersonRole; +} diff --git a/examples/multiple-schemas/queries.gql b/examples/multiple-schemas/queries.gql new file mode 100644 index 000000000..644c394dd --- /dev/null +++ b/examples/multiple-schemas/queries.gql @@ -0,0 +1,21 @@ +query AllPersons { + persons { + id + name + age + role + } +} + +query OneRecipe { + recipe(id: 1) { + uuid + title + ratings + averageRating + } +} + +mutation PromotePeronsOne { + promote(personId: 1) +} diff --git a/examples/multiple-schemas/recipe/recipe.resolver.ts b/examples/multiple-schemas/recipe/recipe.resolver.ts new file mode 100644 index 000000000..d5337e0cb --- /dev/null +++ b/examples/multiple-schemas/recipe/recipe.resolver.ts @@ -0,0 +1,22 @@ +import { Resolver, FieldResolver, Root } from "../../../src"; + +import { ResourceResolver } from "../resource/resource.resolver"; +import { Recipe } from "./recipe.type"; + +const recipes: Recipe[] = [ + { + id: 1, + title: "Recipe 1", + ratings: [1, 3, 4], + }, +]; + +@Resolver(of => Recipe) +export class RecipeResolver extends ResourceResolver(Recipe, recipes) { + // here you can add resource-specific operations + + @FieldResolver() + averageRating(@Root() recipe: Recipe): number { + return recipe.ratings.reduce((a, b) => a + b, 0) / recipe.ratings.length; + } +} diff --git a/examples/multiple-schemas/recipe/recipe.type.ts b/examples/multiple-schemas/recipe/recipe.type.ts new file mode 100644 index 000000000..62edb1a23 --- /dev/null +++ b/examples/multiple-schemas/recipe/recipe.type.ts @@ -0,0 +1,15 @@ +import { ObjectType, Field, Int } from "../../../src"; + +import { Resource } from "../resource/resource"; + +@ObjectType() +export class Recipe implements Resource { + @Field() + id: number; + + @Field() + title: string; + + @Field(type => [Int]) + ratings: number[]; +} diff --git a/examples/multiple-schemas/resource/resource.resolver.ts b/examples/multiple-schemas/resource/resource.resolver.ts new file mode 100644 index 000000000..47bc52aa5 --- /dev/null +++ b/examples/multiple-schemas/resource/resource.resolver.ts @@ -0,0 +1,62 @@ +import { Service } from "typedi"; +import { + Query, + Arg, + Int, + Resolver, + ArgsType, + Field, + Args, + FieldResolver, + Root, + ClassType, +} from "../../../src"; + +import { Resource } from "./resource"; +import { ResourceService, ResourceServiceFactory } from "./resource.service"; + +@ArgsType() +export class GetAllArgs { + @Field(type => Int) + skip: number = 0; + + @Field(type => Int) + take: number = 10; +} + +export function ResourceResolver( + ResourceCls: ClassType, + resources: TResource[], +) { + const resourceName = ResourceCls.name.toLocaleLowerCase(); + + // `isAbstract` decorator option is mandatory to prevent multiple registering in schema + @Resolver(of => ResourceCls, { isAbstract: true }) + @Service() + abstract class ResourceResolverClass { + protected resourceService: ResourceService; + + constructor(factory: ResourceServiceFactory) { + this.resourceService = factory.create(resources); + } + + @Query(returns => ResourceCls, { name: `${resourceName}` }) + protected async getOne(@Arg("id", type => Int) id: number) { + return this.resourceService.getOne(id); + } + + @Query(returns => [ResourceCls], { name: `${resourceName}s` }) + protected async getAll(@Args() { skip, take }: GetAllArgs) { + const all = this.resourceService.getAll(skip, take); + return all; + } + + // dynamically created field with resolver for all child resource classes + @FieldResolver({ name: "uuid" }) + protected getUuid(@Root() resource: Resource): string { + return `${resourceName}_${resource.id}`; + } + } + + return ResourceResolverClass; +} diff --git a/examples/multiple-schemas/resource/resource.service.ts b/examples/multiple-schemas/resource/resource.service.ts new file mode 100644 index 000000000..cfe1286de --- /dev/null +++ b/examples/multiple-schemas/resource/resource.service.ts @@ -0,0 +1,25 @@ +import { Service } from "typedi"; + +import { Resource } from "./resource"; + +// we need to use factory as we need separate instance of service for each generic +@Service() +export class ResourceServiceFactory { + create(resources?: TResource[]) { + return new ResourceService(resources); + } +} + +export class ResourceService { + constructor(protected resources: TResource[] = []) {} + + getOne(id: number): TResource | undefined { + return this.resources.find(res => res.id === id); + } + + getAll(skip: number, take: number): TResource[] { + const start: number = skip; + const end: number = skip + take; + return this.resources.slice(start, end); + } +} diff --git a/examples/multiple-schemas/resource/resource.ts b/examples/multiple-schemas/resource/resource.ts new file mode 100644 index 000000000..c96cc8fd5 --- /dev/null +++ b/examples/multiple-schemas/resource/resource.ts @@ -0,0 +1,3 @@ +export interface Resource { + id: number; +} diff --git a/examples/multiple-schemas/schemaPerson.gql b/examples/multiple-schemas/schemaPerson.gql new file mode 100644 index 000000000..23e80f00f --- /dev/null +++ b/examples/multiple-schemas/schemaPerson.gql @@ -0,0 +1,26 @@ +# ----------------------------------------------- +# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! +# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! +# ----------------------------------------------- + +type Mutation { + promote(personId: Int!): Boolean! +} + +type Person { + id: Float! + name: String! + age: Int! + role: PersonRole! +} + +enum PersonRole { + Normal + Pro + Admin +} + +type Query { + person(id: Int!): Person! + persons(skip: Int = 0, take: Int = 10): [Person!]! +} diff --git a/examples/multiple-schemas/schemaRecipe.gql b/examples/multiple-schemas/schemaRecipe.gql new file mode 100644 index 000000000..3dc918b91 --- /dev/null +++ b/examples/multiple-schemas/schemaRecipe.gql @@ -0,0 +1,15 @@ +# ----------------------------------------------- +# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! +# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! +# ----------------------------------------------- + +type Query { + recipe(id: Int!): Recipe! + recipes(skip: Int = 0, take: Int = 10): [Recipe!]! +} + +type Recipe { + id: Float! + title: String! + ratings: [Int!]! +} diff --git a/src/metadata/metadata-builder.ts b/src/metadata/metadata-builder.ts new file mode 100644 index 000000000..e7132199a --- /dev/null +++ b/src/metadata/metadata-builder.ts @@ -0,0 +1,214 @@ +import { + AuthorizedMetadata, + BaseResolverMetadata, + ClassMetadata, + EnumMetadata, + FieldMetadata, + FieldResolverMetadata, + MiddlewareMetadata, + ParamMetadata, + ResolverClassMetadata, + ResolverMetadata, + SubscriptionResolverMetadata, + UnionMetadataWithSymbol, +} from "./definitions"; +import { getMetadataStorage } from "./getMetadataStorage"; +import { + copyMetadata, + mapMiddlewareMetadataToArray, + mapSuperFieldResolverHandlers, + mapSuperResolverHandlers, +} from "./utils"; +import { ClassType } from "../interfaces"; +import { NoExplicitTypeError } from "../errors"; + +export class MetadataBuilder { + queries: ResolverMetadata[] = []; + mutations: ResolverMetadata[] = []; + subscriptions: SubscriptionResolverMetadata[] = []; + fieldResolvers: FieldResolverMetadata[] = []; + objectTypes: ClassMetadata[] = []; + inputTypes: ClassMetadata[] = []; + argumentTypes: ClassMetadata[] = []; + interfaceTypes: ClassMetadata[] = []; + authorizedFields: AuthorizedMetadata[] = []; + enums: EnumMetadata[] = []; + unions: UnionMetadataWithSymbol[] = []; + middlewares: MiddlewareMetadata[] = []; + + resolverClasses: ResolverClassMetadata[] = []; + + fields: FieldMetadata[] = []; + params: ParamMetadata[] = []; + + build(resolvers: Function[], types: any[]) { + const storage = getMetadataStorage(); + + this.resolverClasses = copyMetadata(storage.resolverClasses).filter( + def => def.isAbstract || resolvers.includes(def.target), + ); + this.middlewares = copyMetadata(storage.middlewares).filter(def => + resolvers.includes(def.target), + ); + + this.objectTypes = copyMetadata(storage.objectTypes).filter(def => types.includes(def.target)); + this.inputTypes = copyMetadata(storage.inputTypes).filter(def => types.includes(def.target)); + this.argumentTypes = copyMetadata(storage.argumentTypes).filter(def => + types.includes(def.target), + ); + this.interfaceTypes = copyMetadata(storage.interfaceTypes).filter(def => + types.includes(def.target), + ); + + this.enums = copyMetadata(storage.enums); + this.unions = copyMetadata(storage.unions); + this.fields = copyMetadata(storage.fields); + this.params = copyMetadata(storage.params); + + const isExistResolver = (def: { target: Function }) => + this.resolverClasses.find(resDef => resDef.target === def.target); + + this.interfaceTypes = copyMetadata(storage.interfaceTypes).filter(isExistResolver); + this.queries = copyMetadata(storage.queries).filter(isExistResolver); + this.mutations = copyMetadata(storage.mutations).filter(isExistResolver); + this.subscriptions = copyMetadata(storage.subscriptions).filter(isExistResolver); + this.authorizedFields = copyMetadata(storage.authorizedFields).filter(isExistResolver); + + this.buildClassMetadata(this.objectTypes); + this.buildClassMetadata(this.inputTypes); + this.buildClassMetadata(this.argumentTypes); + this.buildClassMetadata(this.interfaceTypes); + + this.buildFieldResolverMetadata(this.fieldResolvers); + + this.buildResolversMetadata(this.queries); + this.buildResolversMetadata(this.mutations); + this.buildResolversMetadata(this.subscriptions); + + this.buildExtendedResolversMetadata(resolvers); + } + + private buildClassMetadata(definitions: ClassMetadata[]) { + definitions.forEach(def => { + const fields = this.fields.filter(field => field.target === def.target); + fields.forEach(field => { + field.roles = this.findFieldRoles(field.target, field.name); + field.params = this.params.filter( + param => param.target === field.target && field.name === param.methodName, + ); + field.middlewares = mapMiddlewareMetadataToArray( + this.middlewares.filter( + middleware => middleware.target === field.target && middleware.fieldName === field.name, + ), + ); + }); + def.fields = fields; + }); + } + + private buildResolversMetadata(definitions: BaseResolverMetadata[]) { + definitions.forEach(def => { + const resolverClassMetadata = this.resolverClasses.find( + resolver => resolver.target === def.target, + )!; + def.resolverClassMetadata = resolverClassMetadata; + def.params = this.params.filter( + param => param.target === def.target && def.methodName === param.methodName, + ); + def.roles = this.findFieldRoles(def.target, def.methodName); + def.middlewares = mapMiddlewareMetadataToArray( + this.middlewares.filter( + middleware => middleware.target === def.target && def.methodName === middleware.fieldName, + ), + ); + }); + } + + private buildFieldResolverMetadata(definitions: FieldResolverMetadata[]) { + this.buildResolversMetadata(definitions); + definitions.forEach(def => { + def.roles = this.findFieldRoles(def.target, def.methodName); + def.getObjectType = + def.kind === "external" + ? this.resolverClasses.find(resolver => resolver.target === def.target)!.getObjectType + : () => def.target as ClassType; + if (def.kind === "external") { + const objectTypeCls = this.resolverClasses.find(resolver => resolver.target === def.target)! + .getObjectType!(); + const objectType = this.objectTypes.find( + objTypeDef => objTypeDef.target === objectTypeCls, + )!; + const objectTypeField = objectType.fields!.find( + fieldDef => fieldDef.name === def.methodName, + )!; + if (!objectTypeField) { + if (!def.getType || !def.typeOptions) { + throw new NoExplicitTypeError(def.target.name, def.methodName); + } + const fieldMetadata: FieldMetadata = { + name: def.methodName, + schemaName: def.schemaName, + getType: def.getType!, + target: objectTypeCls, + typeOptions: def.typeOptions!, + deprecationReason: def.deprecationReason, + description: def.description, + complexity: def.complexity, + roles: def.roles!, + middlewares: def.middlewares!, + params: def.params!, + }; + this.fields.push(fieldMetadata); + objectType.fields!.push(fieldMetadata); + } else { + objectTypeField.complexity = def.complexity; + if (objectTypeField.params!.length === 0) { + objectTypeField.params = def.params!; + } + if (def.roles) { + objectTypeField.roles = def.roles; + } else if (objectTypeField.roles) { + def.roles = objectTypeField.roles; + } + } + } + }); + } + + private buildExtendedResolversMetadata(resolvers: Function[]) { + this.resolverClasses + .filter(def => resolvers.includes(def.target)) + .forEach(def => { + const target = def.target; + let superResolver = Object.getPrototypeOf(target); + + // copy and modify metadata of resolver from parent resolver class + while (superResolver.prototype) { + const superResolverMetadata = this.resolverClasses.find( + it => it.target === superResolver, + ); + if (superResolverMetadata) { + this.queries.unshift(...mapSuperResolverHandlers(this.queries, superResolver, def)); + this.mutations.unshift(...mapSuperResolverHandlers(this.mutations, superResolver, def)); + this.subscriptions.unshift( + ...mapSuperResolverHandlers(this.subscriptions, superResolver, def), + ); + this.fieldResolvers.unshift( + ...mapSuperFieldResolverHandlers(this.fieldResolvers, superResolver, def), + ); + } + superResolver = Object.getPrototypeOf(superResolver); + } + }); + } + + private findFieldRoles(target: Function, fieldName: string): any[] | undefined { + const authorizedField = this.authorizedFields.find( + authField => authField.target === target && authField.fieldName === fieldName, + ); + if (!authorizedField) { + return; + } + return authorizedField.roles; + } +} diff --git a/src/metadata/metadata-storage.ts b/src/metadata/metadata-storage.ts index a5de7f335..d2284d03e 100644 --- a/src/metadata/metadata-storage.ts +++ b/src/metadata/metadata-storage.ts @@ -36,9 +36,9 @@ export class MetadataStorage { unions: UnionMetadataWithSymbol[] = []; middlewares: MiddlewareMetadata[] = []; - private resolverClasses: ResolverClassMetadata[] = []; - private fields: FieldMetadata[] = []; - private params: ParamMetadata[] = []; + resolverClasses: ResolverClassMetadata[] = []; + fields: FieldMetadata[] = []; + params: ParamMetadata[] = []; constructor() { ensureReflectMetadataExists(); @@ -96,23 +96,6 @@ export class MetadataStorage { this.params.push(definition); } - build() { - // TODO: disable next build attempts - - this.buildClassMetadata(this.objectTypes); - this.buildClassMetadata(this.inputTypes); - this.buildClassMetadata(this.argumentTypes); - this.buildClassMetadata(this.interfaceTypes); - - this.buildFieldResolverMetadata(this.fieldResolvers); - - this.buildResolversMetadata(this.queries); - this.buildResolversMetadata(this.mutations); - this.buildResolversMetadata(this.subscriptions); - - this.buildExtendedResolversMetadata(); - } - clear() { this.queries = []; this.mutations = []; @@ -131,124 +114,4 @@ export class MetadataStorage { this.fields = []; this.params = []; } - - private buildClassMetadata(definitions: ClassMetadata[]) { - definitions.forEach(def => { - const fields = this.fields.filter(field => field.target === def.target); - fields.forEach(field => { - field.roles = this.findFieldRoles(field.target, field.name); - field.params = this.params.filter( - param => param.target === field.target && field.name === param.methodName, - ); - field.middlewares = mapMiddlewareMetadataToArray( - this.middlewares.filter( - middleware => middleware.target === field.target && middleware.fieldName === field.name, - ), - ); - }); - def.fields = fields; - }); - } - - private buildResolversMetadata(definitions: BaseResolverMetadata[]) { - definitions.forEach(def => { - const resolverClassMetadata = this.resolverClasses.find( - resolver => resolver.target === def.target, - )!; - def.resolverClassMetadata = resolverClassMetadata; - def.params = this.params.filter( - param => param.target === def.target && def.methodName === param.methodName, - ); - def.roles = this.findFieldRoles(def.target, def.methodName); - def.middlewares = mapMiddlewareMetadataToArray( - this.middlewares.filter( - middleware => middleware.target === def.target && def.methodName === middleware.fieldName, - ), - ); - }); - } - - private buildFieldResolverMetadata(definitions: FieldResolverMetadata[]) { - this.buildResolversMetadata(definitions); - definitions.forEach(def => { - def.roles = this.findFieldRoles(def.target, def.methodName); - def.getObjectType = - def.kind === "external" - ? this.resolverClasses.find(resolver => resolver.target === def.target)!.getObjectType - : () => def.target as ClassType; - if (def.kind === "external") { - const objectTypeCls = this.resolverClasses.find(resolver => resolver.target === def.target)! - .getObjectType!(); - const objectType = this.objectTypes.find( - objTypeDef => objTypeDef.target === objectTypeCls, - )!; - const objectTypeField = objectType.fields!.find( - fieldDef => fieldDef.name === def.methodName, - )!; - if (!objectTypeField) { - if (!def.getType || !def.typeOptions) { - throw new NoExplicitTypeError(def.target.name, def.methodName); - } - const fieldMetadata: FieldMetadata = { - name: def.methodName, - schemaName: def.schemaName, - getType: def.getType!, - target: objectTypeCls, - typeOptions: def.typeOptions!, - deprecationReason: def.deprecationReason, - description: def.description, - complexity: def.complexity, - roles: def.roles!, - middlewares: def.middlewares!, - params: def.params!, - }; - this.collectClassFieldMetadata(fieldMetadata); - objectType.fields!.push(fieldMetadata); - } else { - objectTypeField.complexity = def.complexity; - if (objectTypeField.params!.length === 0) { - objectTypeField.params = def.params!; - } - if (def.roles) { - objectTypeField.roles = def.roles; - } else if (objectTypeField.roles) { - def.roles = objectTypeField.roles; - } - } - } - }); - } - - private buildExtendedResolversMetadata() { - this.resolverClasses.forEach(def => { - const target = def.target; - let superResolver = Object.getPrototypeOf(target); - - // copy and modify metadata of resolver from parent resolver class - while (superResolver.prototype) { - const superResolverMetadata = this.resolverClasses.find(it => it.target === superResolver); - if (superResolverMetadata) { - this.queries.unshift(...mapSuperResolverHandlers(this.queries, superResolver, def)); - this.mutations.unshift(...mapSuperResolverHandlers(this.mutations, superResolver, def)); - this.subscriptions.unshift( - ...mapSuperResolverHandlers(this.subscriptions, superResolver, def), - ); - this.fieldResolvers.unshift( - ...mapSuperFieldResolverHandlers(this.fieldResolvers, superResolver, def), - ); - } - superResolver = Object.getPrototypeOf(superResolver); - } - }); - } - - private findFieldRoles(target: Function, fieldName: string): any[] | undefined { - const authorizedField = this.authorizedFields.find( - authField => authField.target === target && authField.fieldName === fieldName, - ); - if (!authorizedField) { - return; - } - return authorizedField.roles; - } } diff --git a/src/metadata/utils.ts b/src/metadata/utils.ts index 5e100d492..bf3912f1d 100644 --- a/src/metadata/utils.ts +++ b/src/metadata/utils.ts @@ -8,6 +8,10 @@ import { Middleware } from "../interfaces/Middleware"; import { isThrowing } from "../helpers/isThrowing"; import { ReflectMetadataMissingError } from "../errors"; +export function copyMetadata(array: T[]): T[] { + return array.map(item => Object.assign({}, item)); +} + export function mapSuperResolverHandlers( definitions: T[], superResolver: Function, diff --git a/src/schema/schema-generator.ts b/src/schema/schema-generator.ts index 09fe46341..1366ab5ef 100644 --- a/src/schema/schema-generator.ts +++ b/src/schema/schema-generator.ts @@ -17,7 +17,7 @@ import { } from "graphql"; import { withFilter, ResolverFn } from "graphql-subscriptions"; -import { getMetadataStorage } from "../metadata/getMetadataStorage"; +import { MetadataBuilder } from "../metadata/metadata-builder"; import { ResolverMetadata, ParamMetadata, @@ -68,34 +68,41 @@ interface UnionTypeInfo { } export interface SchemaGeneratorOptions extends BuildContextOptions { + resolvers: Function[]; + types: any[]; + /** * Disable checking on build the correctness of a schema */ skipCheck?: boolean; } -export abstract class SchemaGenerator { - private static objectTypesInfo: ObjectTypeInfo[] = []; - private static inputTypesInfo: InputObjectTypeInfo[] = []; - private static interfaceTypesInfo: InterfaceTypeInfo[] = []; - private static enumTypesInfo: EnumTypeInfo[] = []; - private static unionTypesInfo: UnionTypeInfo[] = []; +export class SchemaGenerator { + private objectTypesInfo: ObjectTypeInfo[] = []; + private inputTypesInfo: InputObjectTypeInfo[] = []; + private interfaceTypesInfo: InterfaceTypeInfo[] = []; + private enumTypesInfo: EnumTypeInfo[] = []; + private unionTypesInfo: UnionTypeInfo[] = []; + private metadataBuilder = new MetadataBuilder(); - static async generateFromMetadata(options: SchemaGeneratorOptions): Promise { + async generateFromMetadata(options: SchemaGeneratorOptions): Promise { const schema = this.generateFromMetadataSync(options); if (!options.skipCheck) { const { errors } = await graphql(schema, getIntrospectionQuery()); if (errors) { + console.error(errors); throw new GeneratingSchemaError(errors); } } return schema; } - static generateFromMetadataSync(options: SchemaGeneratorOptions): GraphQLSchema { + generateFromMetadataSync(options: SchemaGeneratorOptions): GraphQLSchema { this.checkForErrors(options); BuildContext.create(options); - getMetadataStorage().build(); + + this.metadataBuilder.build(options.resolvers, options.types); + this.buildTypesInfo(); const schema = new GraphQLSchema({ @@ -109,16 +116,16 @@ export abstract class SchemaGenerator { return schema; } - private static checkForErrors(options: SchemaGeneratorOptions) { + private checkForErrors(options: SchemaGeneratorOptions) { ensureInstalledCorrectGraphQLPackage(); - if (getMetadataStorage().authorizedFields.length !== 0 && options.authChecker === undefined) { + if (this.metadataBuilder.authorizedFields.length !== 0 && options.authChecker === undefined) { throw new Error( "You need to provide `authChecker` function for `@Authorized` decorator usage!", ); } } - private static getDefaultValue( + private getDefaultValue( typeInstance: { [property: string]: unknown }, typeOptions: TypeOptions, fieldName: string, @@ -142,8 +149,8 @@ export abstract class SchemaGenerator { : defaultValueFromInitializer; } - private static buildTypesInfo() { - this.unionTypesInfo = getMetadataStorage().unions.map(unionMetadata => { + private buildTypesInfo() { + this.unionTypesInfo = this.metadataBuilder.unions.map(unionMetadata => { return { unionSymbol: unionMetadata.symbol, type: new GraphQLUnionType({ @@ -164,7 +171,7 @@ export abstract class SchemaGenerator { }), }; }); - this.enumTypesInfo = getMetadataStorage().enums.map(enumMetadata => { + this.enumTypesInfo = this.metadataBuilder.enums.map(enumMetadata => { const enumMap = getEnumValuesMap(enumMetadata.enumObj); return { enumObj: enumMetadata.enumObj, @@ -180,7 +187,7 @@ export abstract class SchemaGenerator { }), }; }); - this.interfaceTypesInfo = getMetadataStorage().interfaceTypes.map( + this.interfaceTypesInfo = this.metadataBuilder.interfaceTypes.map( interfaceType => { const interfaceSuperClass = Object.getPrototypeOf(interfaceType.target); const hasExtended = interfaceSuperClass.prototype !== undefined; @@ -190,8 +197,8 @@ export abstract class SchemaGenerator { ); return superClassTypeInfo ? superClassTypeInfo.type : undefined; }; - const implementingObjectTypesTargets = getMetadataStorage() - .objectTypes.filter( + const implementingObjectTypesTargets = this.metadataBuilder.objectTypes + .filter( objectType => objectType.interfaceClasses && objectType.interfaceClasses.includes(interfaceType.target), @@ -238,7 +245,7 @@ export abstract class SchemaGenerator { }, ); - this.objectTypesInfo = getMetadataStorage().objectTypes.map(objectType => { + this.objectTypesInfo = this.metadataBuilder.objectTypes.map(objectType => { const objectSuperClass = Object.getPrototypeOf(objectType.target); const hasExtended = objectSuperClass.prototype !== undefined; const getSuperClassType = () => { @@ -272,7 +279,7 @@ export abstract class SchemaGenerator { fields: () => { let fields = objectType.fields!.reduce>( (fieldsMap, field) => { - const fieldResolverMetadata = getMetadataStorage().fieldResolvers.find( + const fieldResolverMetadata = this.metadataBuilder.fieldResolvers.find( resolver => resolver.getObjectType!() === objectType.target && resolver.methodName === field.name && @@ -320,7 +327,7 @@ export abstract class SchemaGenerator { }; }); - this.inputTypesInfo = getMetadataStorage().inputTypes.map(inputType => { + this.inputTypesInfo = this.metadataBuilder.inputTypes.map(inputType => { const objectSuperClass = Object.getPrototypeOf(inputType.target); const getSuperClassType = () => { const superClassTypeInfo = this.inputTypesInfo.find( @@ -369,34 +376,34 @@ export abstract class SchemaGenerator { }); } - private static buildRootQueryType(): GraphQLObjectType { + private buildRootQueryType(): GraphQLObjectType { return new GraphQLObjectType({ name: "Query", - fields: this.generateHandlerFields(getMetadataStorage().queries), + fields: this.generateHandlerFields(this.metadataBuilder.queries), }); } - private static buildRootMutationType(): GraphQLObjectType | undefined { - if (getMetadataStorage().mutations.length === 0) { + private buildRootMutationType(): GraphQLObjectType | undefined { + if (this.metadataBuilder.mutations.length === 0) { return; } return new GraphQLObjectType({ name: "Mutation", - fields: this.generateHandlerFields(getMetadataStorage().mutations), + fields: this.generateHandlerFields(this.metadataBuilder.mutations), }); } - private static buildRootSubscriptionType(): GraphQLObjectType | undefined { - if (getMetadataStorage().subscriptions.length === 0) { + private buildRootSubscriptionType(): GraphQLObjectType | undefined { + if (this.metadataBuilder.subscriptions.length === 0) { return; } return new GraphQLObjectType({ name: "Subscription", - fields: this.generateSubscriptionsFields(getMetadataStorage().subscriptions), + fields: this.generateSubscriptionsFields(this.metadataBuilder.subscriptions), }); } - private static buildOtherTypes(): GraphQLNamedType[] { + private buildOtherTypes(): GraphQLNamedType[] { // TODO: investigate the need of directly providing this types // maybe GraphQL can use only the types provided indirectly return [ @@ -406,7 +413,7 @@ export abstract class SchemaGenerator { ]; } - private static generateHandlerFields( + private generateHandlerFields( handlers: ResolverMetadata[], ): GraphQLFieldConfigMap { return handlers.reduce>((fields, handler) => { @@ -430,7 +437,7 @@ export abstract class SchemaGenerator { }, {}); } - private static generateSubscriptionsFields( + private generateSubscriptionsFields( subscriptionsHandlers: SubscriptionResolverMetadata[], ): GraphQLFieldConfigMap { const { pubSub } = BuildContext; @@ -467,7 +474,7 @@ export abstract class SchemaGenerator { }, basicFields); } - private static generateHandlerArgs(params: ParamMetadata[]): GraphQLFieldConfigArgumentMap { + private generateHandlerArgs(params: ParamMetadata[]): GraphQLFieldConfigArgumentMap { return params!.reduce((args, param) => { if (param.kind === "arg") { args[param.name] = { @@ -476,12 +483,12 @@ export abstract class SchemaGenerator { defaultValue: param.typeOptions.defaultValue, }; } else if (param.kind === "args") { - const argumentType = getMetadataStorage().argumentTypes.find( + const argumentType = this.metadataBuilder.argumentTypes.find( it => it.target === param.getType(), )!; let superClass = Object.getPrototypeOf(argumentType.target); while (superClass.prototype !== undefined) { - const superArgumentType = getMetadataStorage().argumentTypes.find( + const superArgumentType = this.metadataBuilder.argumentTypes.find( it => it.target === superClass, )!; if (superArgumentType) { @@ -495,10 +502,7 @@ export abstract class SchemaGenerator { }, {}); } - private static mapArgFields( - argumentType: ClassMetadata, - args: GraphQLFieldConfigArgumentMap = {}, - ) { + private mapArgFields(argumentType: ClassMetadata, args: GraphQLFieldConfigArgumentMap = {}) { const argumentInstance = new (argumentType.target as any)(); argumentType.fields!.forEach(field => { field.typeOptions.defaultValue = this.getDefaultValue( @@ -515,7 +519,7 @@ export abstract class SchemaGenerator { }); } - private static getGraphQLOutputType( + private getGraphQLOutputType( typeOwnerName: string, type: TypeValue, typeOptions: TypeOptions = {}, @@ -554,7 +558,7 @@ export abstract class SchemaGenerator { return wrapWithTypeOptions(typeOwnerName, gqlType, typeOptions, nullableByDefault); } - private static getGraphQLInputType( + private getGraphQLInputType( typeOwnerName: string, type: TypeValue, typeOptions: TypeOptions = {}, diff --git a/src/utils/buildSchema.ts b/src/utils/buildSchema.ts index 7d3bf6fc2..f14a51853 100644 --- a/src/utils/buildSchema.ts +++ b/src/utils/buildSchema.ts @@ -15,8 +15,6 @@ interface EmitSchemaFileOptions extends PrintSchemaOptions { } export interface BuildSchemaOptions extends SchemaGeneratorOptions { - /** Array of resolvers classes or glob paths to resolver files */ - resolvers: Array; /** * Path to the file to where emit the schema * or config object with print schema options @@ -25,8 +23,8 @@ export interface BuildSchemaOptions extends SchemaGeneratorOptions { emitSchemaFile?: string | boolean | EmitSchemaFileOptions; } export async function buildSchema(options: BuildSchemaOptions): Promise { - loadResolvers(options); - const schema = await SchemaGenerator.generateFromMetadata(options); + checkOptions(options); + const schema = await new SchemaGenerator().generateFromMetadata(options); if (options.emitSchemaFile) { const { schemaFileName, printSchemaOptions } = getEmitSchemaDefinitionFileOptions(options); await emitSchemaDefinitionFile(schemaFileName, schema, printSchemaOptions); @@ -35,8 +33,8 @@ export async function buildSchema(options: BuildSchemaOptions): Promise { - if (typeof resolver === "string") { - loadResolversFromGlob(resolver); - } - }); + if (!options.types || options.types.length === 0) { + throw new Error("Empty `types` array property found in `buildSchema` options!"); + } } function getEmitSchemaDefinitionFileOptions(