Skip to content

Commit

Permalink
Merge pull request #46 from indigotech/feature/interface-decorator
Browse files Browse the repository at this point in the history
Feature - @InterfaceType decorator
  • Loading branch information
felipesabino authored Nov 22, 2017
2 parents 2cc4305 + 0b2bbcf commit 223937c
Show file tree
Hide file tree
Showing 22 changed files with 264 additions and 23 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ 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.
- @UnionType: It can be used to create `GraphQLUnionType` objects.
- @InterfaceType: It can be used to create `GraphQLInterfaceType` objects.

#### GraphQL Decorator Examples

Expand Down
25 changes: 25 additions & 0 deletions src/array.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Concats two arrays
* @param itemsA first array argument
* @param itemsB second array argument
*/
export const concat = (itemsA: any[], itemsB: any[]) => itemsA.concat(itemsB);

/**
* Executes a flatMap modifier function to each elements of the array
* @param λ the flatMap function
* @param collection the array to apply the flatMap funtion to
*/
export const flatMap = (λ: (item: any) => any, collection: any[]) => collection.map(λ).reduce(concat, []);

/**
* Flattens an array with nested arrays
* @param collection the array argument
*/
export const flatten = (collection: any[]) => flatMap(items => {
if (items.constructor === Array) {
return flatMap(item => item, items);
} else {
return items;
}
}, collection);
1 change: 1 addition & 0 deletions src/decorator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './field.decorator';
export * from './order-by.decorator';
export * from './root.decorator';
export * from './before.decorator';
export * from './interface-type.decorator';
19 changes: 19 additions & 0 deletions src/decorator/interface-type.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getMetadataArgsStorage } from '../metadata-builder';
import { InterfaceOption } from '../index';

/**
* Interface Type.
* See [GraphQL Documentation - Interfaces]{@link http://graphql.org/learn/schema/#interfaces}
*
* @param option Options for an Interface definition
*/
export function InterfaceType<T>(option: InterfaceOption<T>) {
return function (target: any) {
getMetadataArgsStorage().interfaces.push({
target: target,
name: target.name,
resolver: option.resolver,
description: option.description,
});
} as Function;
}
11 changes: 11 additions & 0 deletions src/decorator/object-type.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SchemaFactoryError, SchemaFactoryErrorType } from '../type-factory';

import { ObjectOption } from '../metadata/options';
import { getMetadataArgsStorage } from '../metadata-builder';

Expand All @@ -23,11 +25,20 @@ export function InputObjectType(option?: ObjectOption) {

function CreateObjectType(isInput: boolean, option?: ObjectOption) {
return function (target: any) {

if (isInput && option && option.interfaces) {
throw new SchemaFactoryError(`Input types are not allowed to have interfaces: '${target.name}'`,
SchemaFactoryErrorType.INPUT_FIELD_SHOULD_NOT_HAVE_INTERFACE);
}

getMetadataArgsStorage().objects.push({
target: target,
name: target.name,
description: option ? option.description : null,
isInput: isInput,
interfaces: option && option.interfaces ? (
option.interfaces.constructor !== Array ? [option.interfaces as Function] : option.interfaces as Function[]
) : [],
});
};
}
Expand Down
6 changes: 6 additions & 0 deletions src/metadata-builder/metadata-args.storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
RootArg,
SchemaArg,
UnionTypeArg,
InterfaceTypeArg,
} from '../metadata/args';

import { MetadataUtils } from './metadata.utils';
Expand All @@ -29,6 +30,7 @@ export class MetadataArgsStorage {
roots: RootArg[] = [];
orderBys: OrderByArg[] = [];
befores: BeforeArg[] = [];
interfaces: InterfaceTypeArg[] = [];

filterEnumsByClass(target: any): EnumTypeArg[] {
return this.enums.filter(item => item.target === target);
Expand All @@ -42,6 +44,10 @@ export class MetadataArgsStorage {
return this.union.filter(item => item.target === target);
}

filterInterfaceTypeByClass(target: any): InterfaceTypeArg[] {
return this.interfaces.filter(item => item.target === target);
}

filterObjectTypeByClass(target: any): ObjectTypeArg[] {
return this.objects.filter(item => item.target === target);
}
Expand Down
14 changes: 14 additions & 0 deletions src/metadata-builder/metadata.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {
RootMetadata,
SchemaMetadata,
UnionTypeMetadata,
InterfaceTypeMetadata,
} from '../metadata/types';

import { flatten } from '../array.utils';
import { EntryType } from '../metadata/args';
import { getMetadataArgsStorage } from './metadata-args.storage';

Expand Down Expand Up @@ -41,6 +43,17 @@ export class MetadataBuilder {
}));
}

buildInterfaceTypeMetadata(target: any): InterfaceTypeMetadata[] | undefined {
return getMetadataArgsStorage()
.filterInterfaceTypeByClass(target)
.map(arg => ({
target: arg.target,
name: arg.name,
resolver: arg.resolver,
description: arg.description,
}));
}

buildObjectTypeMetadata(target: any): ObjectTypeMetadata[] | undefined {
return getMetadataArgsStorage()
.filterObjectTypeByClass(target)
Expand All @@ -49,6 +62,7 @@ export class MetadataBuilder {
name: arg.name,
description: arg.description,
isInput: arg.isInput,
interfaces: flatten(arg.interfaces.map(this.buildInterfaceTypeMetadata)),
}));
}

Expand Down
1 change: 1 addition & 0 deletions src/metadata/args/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './context.arg';
export * from './root.arg';
export * from './order-by.arg';
export * from './before.arg';
export * from './interface-type.arg';
5 changes: 5 additions & 0 deletions src/metadata/args/interface-type.arg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Argument } from './argument';

export interface InterfaceTypeArg extends Argument {
resolver: (obj: any, context: any, info: any) => Promise<string> | string | null;
}
1 change: 1 addition & 0 deletions src/metadata/args/object-type.arg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { Argument } from './argument';

export interface ObjectTypeArg extends Argument {
isInput: boolean;
interfaces: Function[];
}
1 change: 1 addition & 0 deletions src/metadata/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './field.option';
export * from './argument.option';
export * from './order-by.option';
export * from './before.option';
export * from './interface.option';
11 changes: 11 additions & 0 deletions src/metadata/options/interface.option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Option } from './option';

/**
* Arguments for an {@link InterfaceType} on graphql schema
*/
export interface InterfaceOption<T> extends Option {
/**
* Resolver function to inform schema what type should be returned based on the value provided
*/
resolver: (obj: T, context: any, info: any) => Promise<string> | string | null;
}
4 changes: 3 additions & 1 deletion src/metadata/options/object.option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ import { Option } from './option';
/**
* Arguments for an {@link ObjectType} on graphql schema
*/
export interface ObjectOption extends Option { }
export interface ObjectOption extends Option {
interfaces?: Function | Function[];
}
1 change: 1 addition & 0 deletions src/metadata/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './context.metadata';
export * from './order-by.metadata';
export * from './root.metadata';
export * from './field.metadata';
export * from './interface.metadata';
5 changes: 5 additions & 0 deletions src/metadata/types/interface.metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Metadata } from './metadata';

export interface InterfaceTypeMetadata extends Metadata {
resolver: (obj: any, context: any, info: any) => Promise<string> | string | null;
}
2 changes: 2 additions & 0 deletions src/metadata/types/object.metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Metadata } from './metadata';
import { InterfaceTypeMetadata } from './interface.metadata';

export interface ObjectTypeMetadata extends Metadata {
isInput: boolean;
interfaces: InterfaceTypeMetadata[];
}
77 changes: 75 additions & 2 deletions src/specs/functional.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ describe('Functional', function () {

});

describe('Root', function() {
describe('Root', function () {

@D.ObjectType()
class ReturnType {
Expand Down Expand Up @@ -334,14 +334,87 @@ describe('Functional', function () {
@D.Query() query: QueryType;
}

it('resolves value from provided root object', async function() {
it('resolves value from provided root object', async function () {
const schema = schemaFactory(SchemaType);
const result = await graphql.graphql(schema, `query { field { valueFromProvidedRoot } } `);
assert(result.data.field.valueFromProvidedRoot === 'Hello, world!');
});

});

describe('Interfaces', function () {

@D.InterfaceType({
resolver: (obj: any): string | null => {
// tslint:disable:no-use-before-declare
if (obj.objectField) { return MyObjectType.name; }
if (obj.otherObjectField) { return MyOtherObjectType.name; }
return null;
// tslint:enable:no-use-before-declare
},
})
class MyInterface {
@D.Field()
interfaceField: string;
}

@D.ObjectType({ interfaces: MyInterface })
class MyObjectType {
@D.Field()
objectField: string;
}

@D.ObjectType({ interfaces: [MyInterface] })
class MyOtherObjectType {
@D.Field()
otherObjectField: string;
}

@D.ObjectType()
class QueryType {
@D.Field({ type: MyInterface, isList: true })
value(): any[] {
return [
{
interfaceField: 'A',
objectField: 'objectField',
},
{
interfaceField: 'B',
otherObjectField: 'otherObjectField',
},
];
}
}

@D.Schema()
class SchemaType {
@D.Query() query: QueryType;
}

it('resolves interfaces', async function () {
const schema = schemaFactory(SchemaType);

const result = await graphql.graphql(schema, `
query {
value {
interfaceField
...on MyObjectType {
objectField
}
...on MyOtherObjectType {
otherObjectField
}
}
}
`);
assert(result.data.value.length === 2);
assert(result.data.value[0].interfaceField === 'A');
assert(result.data.value[0].objectField === 'objectField');
assert(result.data.value[1].interfaceField === 'B');
assert(result.data.value[1].otherObjectField === 'otherObjectField');
});
});

});

Expand Down
3 changes: 3 additions & 0 deletions src/type-factory/field.type-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { enumTypeFactory } from './enum.type-factory';
import { getMetadataArgsStorage } from '../metadata-builder';
import { objectTypeFactory } from './object.type-factory';
import { unionTypeFactory } from './union.type-factory';
import { interfaceTypeFactory } from './interface.type-factory';

export interface ResolverHolder {
fn: Function;
Expand Down Expand Up @@ -51,6 +52,8 @@ function convertType(typeFn: Function, metadata: FieldMetadata | ArgumentMetadat
} else if (returnType && returnType.prototype && getMetadataArgsStorage().filterObjectTypeByClass(returnType).length > 0) {
// recursively call objectFactory
returnType = objectTypeFactory(returnType, isInput);
} else if (returnType && returnType.prototype && getMetadataArgsStorage().filterInterfaceTypeByClass(returnType).length > 0) {
returnType = interfaceTypeFactory(returnType);
} else if (returnType && returnType.prototype && getMetadataArgsStorage().filterEnumsByClass(returnType).length > 0) {
returnType = enumTypeFactory(returnType);
}
Expand Down
32 changes: 32 additions & 0 deletions src/type-factory/interface.type-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as graphql from 'graphql';

import { getMetadataBuilder } from '../metadata-builder';
import { objectTypeFactory } from './object.type-factory';
import { fieldTypeFactory } from './field.type-factory';

let interfaceTypeCache: { [key: string]: any } = {};

export function interfaceTypeFactory(target: any): graphql.GraphQLInterfaceType | undefined {
return getMetadataBuilder().buildInterfaceTypeMetadata(target)
.map(metadata => {
if (!interfaceTypeCache[metadata.name]) {
interfaceTypeCache[metadata.name] = new graphql.GraphQLInterfaceType({
description: metadata.description,
name: metadata.name,
resolveType: metadata.resolver,
fields: getMetadataBuilder()
.buildFieldMetadata(target.prototype)
.map(field => ({
metadata: field,
type: fieldTypeFactory(target, field),
}))
.reduce((fields, field) => {
fields[field.metadata.name] = field.type;
return fields;
} , {} as { [key: string]: any}),
});
}
return interfaceTypeCache[metadata.name];
})
.find((_, index) => index === 0);
}
Loading

0 comments on commit 223937c

Please sign in to comment.