diff --git a/README.md b/README.md index 6ae38b3..2cfedbe 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Apart from the decorators listed on the original documentation, we have added si - @Query: It can be used multiple times on the same file. This way we make it possible to break queries into different folders. - @Mutation: It can be used multiple times on the same file. This way we make it possible to break queries into different folders. - @UseContainer: Sets the IoC container to be used in order to instantiate the decorated clas. +- @Uniontype: It can be used to create `GraphQLUnionType` objects. #### GraphQL Decorator Examples diff --git a/src/decorator.ts b/src/decorator.ts index 0bfb288..c913216 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -8,6 +8,9 @@ import { OrderByTypeFactory } from './order-by.type-factory'; import { PageInfo } from './page-info.type'; import { PaginationResponse } from './pagination.type'; +export * from './decorator/'; +export * from './metadata/options'; + export const GQ_QUERY_KEY = 'gq_query'; export const GQ_MUTATION_KEY = 'gq_mutation'; export const GQ_SUBSCRIPTION_KEY = 'gq_subscription'; @@ -17,8 +20,6 @@ export const GQ_OBJECT_METADATA_KEY = 'gq_object_type'; export const GQ_ENUM_METADATA_KEY = 'gq_enum_type'; export const GQ_DESCRIPTION_KEY = 'gq_description'; - - export interface TypeMetadata { name?: string; description?: string; diff --git a/src/decorator/index.ts b/src/decorator/index.ts new file mode 100644 index 0000000..9b2030a --- /dev/null +++ b/src/decorator/index.ts @@ -0,0 +1 @@ +export * from './union-type.decorator'; diff --git a/src/decorator/union-type.decorator.ts b/src/decorator/union-type.decorator.ts new file mode 100644 index 0000000..59f7e37 --- /dev/null +++ b/src/decorator/union-type.decorator.ts @@ -0,0 +1,17 @@ +import { MetadataStorage } from '../metadata-storage'; +import { UnionOption } from '../metadata'; + +/** + * Union Type. ref: http://graphql.org/learn/schema/#union-types + * @param option Options for a Union Type + */ +export function UnionType(option: UnionOption) { + return function (target: any) { + MetadataStorage.addUnionMetadata({ + name: target.name, + types: option.types, + resolver: option.resolver, + description: option.description, + }); + } as Function; +} diff --git a/src/field_type_factory.ts b/src/field_type_factory.ts index 067d784..453c967 100644 --- a/src/field_type_factory.ts +++ b/src/field_type_factory.ts @@ -11,6 +11,8 @@ import { RootMetadata, TypeMetadata, } from './decorator'; +import { MetadataStorage } from './metadata-storage'; + import { SchemaFactoryError, SchemaFactoryErrorType } from './schema_factory'; import { IoCContainer } from './ioc-container'; @@ -18,6 +20,8 @@ import { OrderByTypeFactory } from './order-by.type-factory'; import { PaginationType } from './pagination.type'; import { enumTypeFactory } from './enum.type-factory'; import { objectTypeFactory } from './object_type_factory'; +import { unionTypeFactory } from './type-factory'; + export interface ResolverHolder { fn: Function; @@ -43,12 +47,15 @@ function convertType(typeFn: Function, metadata: TypeMetadata, isInput: boolean, } else if (typeFn === Boolean) { returnType = graphql.GraphQLBoolean; } else if (typeFn && typeFn.prototype && Reflect.hasMetadata(GQ_OBJECT_METADATA_KEY, typeFn.prototype)) { - // recursively call objectFactory - returnType = objectTypeFactory(typeFn, isInput); + // recursively call objectFactory + returnType = objectTypeFactory(typeFn, isInput); } - } else { + } else { returnType = metadata.explicitType; - if (returnType && returnType.prototype && Reflect.hasMetadata(GQ_OBJECT_METADATA_KEY, returnType.prototype)) { + + if (returnType && returnType.prototype && MetadataStorage.containsUnionMetadata(returnType.name)) { + returnType = unionTypeFactory(returnType.name, isInput); + } else if (returnType && returnType.prototype && Reflect.hasMetadata(GQ_OBJECT_METADATA_KEY, returnType.prototype)) { // recursively call objectFactory returnType = objectTypeFactory(returnType, isInput); } else if (returnType && returnType.prototype && Reflect.hasMetadata(GQ_ENUM_METADATA_KEY, returnType.prototype)) { diff --git a/src/metadata-storage/index.ts b/src/metadata-storage/index.ts new file mode 100644 index 0000000..c3c7e5d --- /dev/null +++ b/src/metadata-storage/index.ts @@ -0,0 +1 @@ +export * from './metadata-storage'; diff --git a/src/metadata-storage/metadata-storage.ts b/src/metadata-storage/metadata-storage.ts new file mode 100644 index 0000000..e203274 --- /dev/null +++ b/src/metadata-storage/metadata-storage.ts @@ -0,0 +1,18 @@ +import { UnionTypeMetadata } from '../metadata/types/union.metadata'; + +const unionMetadata: UnionTypeMetadata[] = []; + +const MetadataStorage = { + + addUnionMetadata: function(metadata: UnionTypeMetadata) { + unionMetadata.push(metadata); + }, + getUnionMetadata: function(): UnionTypeMetadata[] { + return unionMetadata; + }, + containsUnionMetadata: function(name: string) { + return unionMetadata.some(metadata => metadata.name === name); + }, +}; + +export { MetadataStorage }; diff --git a/src/metadata/index.ts b/src/metadata/index.ts new file mode 100644 index 0000000..e83e2b5 --- /dev/null +++ b/src/metadata/index.ts @@ -0,0 +1,2 @@ +export * from './options'; +export * from './types'; diff --git a/src/metadata/options/index.ts b/src/metadata/options/index.ts new file mode 100644 index 0000000..3829125 --- /dev/null +++ b/src/metadata/options/index.ts @@ -0,0 +1 @@ +export * from './union.option'; diff --git a/src/metadata/options/union.option.ts b/src/metadata/options/union.option.ts new file mode 100644 index 0000000..cbbf7ae --- /dev/null +++ b/src/metadata/options/union.option.ts @@ -0,0 +1,19 @@ +/** + * Arguments for a Union type on graphql schema + */ +export interface UnionOption { + /** + * (Optional) Description + */ + description?: string; + + /** + * Concrete object types + */ + types: any[]; + + /** + * Resolver function to inform schema what type should be returned based on the value provided + */ + resolver: (obj: T, context: any, info: any) => Promise | string | null; +} diff --git a/src/metadata/types/index.ts b/src/metadata/types/index.ts new file mode 100644 index 0000000..d88db6f --- /dev/null +++ b/src/metadata/types/index.ts @@ -0,0 +1 @@ +export * from './union.metadata'; diff --git a/src/metadata/types/union.metadata.ts b/src/metadata/types/union.metadata.ts new file mode 100644 index 0000000..66c218f --- /dev/null +++ b/src/metadata/types/union.metadata.ts @@ -0,0 +1,6 @@ +export interface UnionTypeMetadata { + name: string; + description?: string; + types: any[]; + resolver: (obj: any, context: any, info: any) => Promise | string | null; +} diff --git a/src/schema_factory.spec.ts b/src/schema_factory.spec.ts index 6ad32bd..f4b6ca5 100644 --- a/src/schema_factory.spec.ts +++ b/src/schema_factory.spec.ts @@ -354,4 +354,54 @@ describe('schemaFactory', function() { }); + describe('UnionType', () => { + + it('creates schema with union type', () => { + + @D.ObjectType() + class ObjA { @D.Field() fieldA: string; } + + @D.ObjectType() + class ObjB { @D.Field() fieldB: string; } + + type MyType = ObjA | ObjB; + @D.UnionType({ + types: [ObjA, ObjB], + resolver: (obj: any): string | null => { + if (obj.fieldA) { return ObjA.name; } + if (obj.fieldB) { return ObjB.name; } + return null; + }, + }) + class MyUnionType { } + + + @D.ObjectType() class Query { + @D.Field({ type: MyUnionType }) + async aQuery(): Promise { + return { fieldA: '' }; + } + } + @D.Schema() class Schema { @D.Query() query: Query; } + const schema = schemaFactory(Schema); + const ast = parse(` + query { + aQuery { + ...on ObjA { + fieldA + } + ...on ObjB { + fieldB + } + } + }`); + + assert.deepEqual(validate(schema, ast), []); + + }); + + }); + + + }); diff --git a/src/schema_factory.ts b/src/schema_factory.ts index c644011..6adb0fa 100644 --- a/src/schema_factory.ts +++ b/src/schema_factory.ts @@ -1,9 +1,9 @@ import * as graphql from 'graphql'; import { FieldTypeMetadata, GQ_FIELDS_KEY, GQ_MUTATION_KEY, GQ_OBJECT_METADATA_KEY, GQ_QUERY_KEY, GQ_SUBSCRIPTION_KEY } from './decorator'; +import { GraphQLObjectType, GraphQLSchema } from 'graphql'; import { mutationObjectTypeFactory, queryObjectTypeFactory, subscriptionObjectTypeFactory } from './object_type_factory'; -import { GraphQLSchema, GraphQLObjectType } from 'graphql'; import { fieldTypeFactory } from './field_type_factory'; export enum SchemaFactoryErrorType { diff --git a/src/type-factory/index.ts b/src/type-factory/index.ts new file mode 100644 index 0000000..6544cd6 --- /dev/null +++ b/src/type-factory/index.ts @@ -0,0 +1 @@ +export * from './union.type-factory'; diff --git a/src/type-factory/union.type-factory.ts b/src/type-factory/union.type-factory.ts new file mode 100644 index 0000000..387a987 --- /dev/null +++ b/src/type-factory/union.type-factory.ts @@ -0,0 +1,20 @@ +import * as graphql from 'graphql'; +import { MetadataStorage } from '../metadata-storage'; +import { UnionTypeMetadata } from '../metadata'; +import { objectTypeFactory } from '../object_type_factory'; + +export function unionTypeFactory(name: string, isInput: boolean): graphql.GraphQLUnionType | undefined { + return MetadataStorage.getUnionMetadata() + .filter(union => union.name === name) + .map(union => { + return new graphql.GraphQLUnionType({ + description: union.description, + name: union.name, + resolveType: union.resolver, + types: union.types + .map(type => objectTypeFactory(type, isInput)) + .filter(_ => _), //filter null values + }); + }) + .find((_, index) => index === 0); +}