Skip to content

Commit

Permalink
feat: allow use of enums with any values
Browse files Browse the repository at this point in the history
fix: nullable/wrapper issues
  • Loading branch information
stephentuso committed Nov 28, 2018
1 parent 1fda153 commit f8cd7e4
Show file tree
Hide file tree
Showing 15 changed files with 401 additions and 116 deletions.
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -41,7 +41,9 @@
"typescript": "^3.1.6"
},
"dependencies": {
"constant-case": "^2.0.0",
"lodash": "^4.17.11",
"pascal-case": "^2.0.1",
"reflect-metadata": "^0.1.12"
},
"peerDependencies": {
Expand Down
120 changes: 120 additions & 0 deletions src/__tests__/typeHelpers.test.ts
@@ -0,0 +1,120 @@
import 'jest';
import ObjectType from '../decorators/ObjectType';
import Field from '../decorators/Field';
import InputObjectType from '../decorators/InputObjectType';
import InputField from '../decorators/InputField';
import nullable from '../wrappers/nullable';
import { getInputType, getNamedType, getOutputType, TSGraphQLString } from '..';
import InterfaceType from '../decorators/InterfaceType';
import {
isEnumType,
isInputObjectType, isInterfaceType,
isListType,
isNonNullType,
isObjectType,
isScalarType,
isUnionType,
} from 'graphql';
import { getType } from '../typeHelpers';
import list from '../wrappers/list';
import TSGraphQLEnumType from '../wrappers/TSGraphQLEnumType';
import TSGraphQLUnionType from '../wrappers/TSGraphQLUnionType';

@ObjectType()
class AnObjectType {
@Field()
foo!: string;
}

@ObjectType()
class AnotherObjectType {
@Field()
bar!: string;
}

@InputObjectType()
class AnInputObjectType {
@InputField()
foo!: string;
}

@InterfaceType()
class AnInterfaceType {
@Field()
foo!: string;
}

enum AnEnum {
Foo,
}

const AnEnumType = new TSGraphQLEnumType(AnEnum, { name: 'AnEnumType' })

const AUnionType = new TSGraphQLUnionType<AnObjectType | AnotherObjectType>({
name: 'AUnionType',
types: [AnObjectType, AnotherObjectType]
})

const ANullableType = nullable(TSGraphQLString);
const AListType = list(TSGraphQLString);

describe('typeHelpers', () => {
describe('getType', () => {
it('should never return GraphQLNonNull if nonNull is false', () => {
expect(isNonNullType(getType(AnObjectType))).toBeFalsy();
expect(isNonNullType(getType(AnInputObjectType))).toBeFalsy();
expect(isNonNullType(getType(AnInterfaceType))).toBeFalsy();
expect(isNonNullType(getType(ANullableType))).toBeFalsy();
expect(isNonNullType(getType(TSGraphQLString))).toBeFalsy();
});

it('should return GraphQLNonNull if nonNull true and type not nullable', () => {
expect(isNonNullType(getType(ANullableType, true))).toBeFalsy();

expect(isNonNullType(getType(AnObjectType, true))).toBeTruthy();
expect(isNonNullType(getType(AnInputObjectType, true))).toBeTruthy();
expect(isNonNullType(getType(AnInterfaceType, true))).toBeTruthy();
expect(isNonNullType(getType(TSGraphQLString, true))).toBeTruthy();
});
});

describe('getInputType', () => {
it('should correctly return input types', () => {
expect(isEnumType(getInputType(AnEnumType))).toBeTruthy();
expect(isScalarType(getInputType(TSGraphQLString))).toBeTruthy();
expect(isInputObjectType(getInputType(AnInputObjectType))).toBeTruthy();
expect(isListType(getInputType(AListType))).toBeTruthy();
expect(isNonNullType(getInputType(TSGraphQLString, true))).toBeTruthy();
});

it('should throw if not an input type', () => {
expect(() => getInputType(AnObjectType)).toThrow();
});
});

describe('getOutputType', () => {
it('should correctly return output types', () => {
expect(isEnumType(getOutputType(AnEnumType))).toBeTruthy();
expect(isScalarType(getOutputType(TSGraphQLString))).toBeTruthy();
expect(isObjectType(getOutputType(AnObjectType))).toBeTruthy();
expect(isListType(getOutputType(AListType))).toBeTruthy();
expect(isInterfaceType(getOutputType(AnInterfaceType))).toBeTruthy();
expect(isNonNullType(getOutputType(TSGraphQLString, true))).toBeTruthy();
});

it('should throw if not an output type', () => {
expect(() => getOutputType(AnInputObjectType)).toThrow();
});
});

describe('getNamedType', () => {
it('should correctly return named types', () => {
expect(isEnumType(getNamedType(AnEnumType))).toBeTruthy();
expect(isScalarType(getNamedType(TSGraphQLString))).toBeTruthy();
expect(isObjectType(getNamedType(AnObjectType))).toBeTruthy();
expect(isInputObjectType(getNamedType(AnInputObjectType))).toBeTruthy();
expect(isUnionType(getNamedType(AUnionType))).toBeTruthy();
expect(isInterfaceType(getNamedType(AnInterfaceType))).toBeTruthy();
});
});
});
27 changes: 27 additions & 0 deletions src/builders/__tests__/getFieldConfigMap.test.ts
Expand Up @@ -8,6 +8,12 @@ import { fields } from '../../fields';
import { ObjectType, TSGraphQLInt, TSGraphQLString } from '../../index';
import Args from '../../decorators/Args';
import Arg from '../../decorators/Arg';
import list from '../../wrappers/list';
import nullable from '../../wrappers/nullable';
import TSGraphQLEnumType, { TSGraphQLEnumCase } from '../../wrappers/TSGraphQLEnumType';
import { Maybe } from '../../types';
import { Wrapper } from '../../wrappers/Wrapper';
import { GraphQLEnumType } from 'graphql';

class Simple {
@Field()
Expand Down Expand Up @@ -185,4 +191,25 @@ describe('getFieldConfigMap', () => {
config.configTest.resolve!(new ArgsTest(), { foo: 'test' }, null, null as any);
config.methodTest.resolve!(new ArgsTest(), { foo: 'test' }, null, null as any);
});

it('should correctly run wrapper transformers', () => {
enum AnEnum {
Foo,
Bar,
}

const AnEnumType = new TSGraphQLEnumType(AnEnum, { name: 'AnEnum', changeCase: TSGraphQLEnumCase.Constant });

@ObjectType()
class Foo {
@Field({ type: list(nullable(AnEnumType)) })
foo() {
return [AnEnum.Foo, AnEnum.Bar, null];
}
}

const config = resolveThunk(getFieldConfigMap(Foo));
expect(config).toHaveProperty('foo');
expect(config.foo!.resolve!(null, null as any, null, null as any)).toEqual(['FOO', 'BAR', null]);
});
});
19 changes: 12 additions & 7 deletions src/builders/getFieldConfigMap.ts
@@ -1,14 +1,14 @@
import { AnyConstructor, EmptyConstructor, ObjectLiteral } from '../types';
import { resolveThunk, Thunk } from '../utils/thunk';
import { GraphQLFieldConfigMap } from 'graphql';
import { GraphQLFieldConfigMap, GraphQLFieldResolver } from 'graphql';
import { FieldConfig, FieldConfigMap, FieldResolver } from '../fields';
import { getFieldConfig, getImplements, getSavedFieldConfigMap } from '../metadata';
import { FieldResolverMethod } from '../decorators/Field';
import { getConstructorChain } from './utils';
import { mapValues } from 'lodash';
import getArgs from './getArgs';
import { getOutputType } from '../typeHelpers';
import { flow } from 'lodash/fp';
import { isWrapper } from '../wrappers/Wrapper';

const convertResolverMethod = (prototype: ObjectLiteral, key: string, Args?: EmptyConstructor<any>): FieldResolver<any, any, any> | null => {
if (typeof prototype[key] === 'function') {
Expand All @@ -24,12 +24,17 @@ const convertResolverMethod = (prototype: ObjectLiteral, key: string, Args?: Emp
const wrapResolver = <TArgs>(
config: FieldConfig<any, any, TArgs>
): FieldConfig<any, any, any> => {
const { resolve, args: Args } = config;
if (resolve && Args) {
const { resolve, type, args: Args } = config;
const transformOutput = isWrapper(type) && type.transformOutput;
if (resolve) {
const wrapped: GraphQLFieldResolver<any, any> = Args
? (source: any, args: ObjectLiteral, ...rest) => resolve(source, Object.assign(new Args(), args), ...rest)
: resolve;
return {
...config,
resolve: (source: any, args: ObjectLiteral, ...rest) =>
resolve(source, Object.assign(new Args(), args), ...rest),
resolve: transformOutput
? (...args) => transformOutput(wrapped(...args))
: wrapped,
}
}
return config;
Expand Down Expand Up @@ -72,7 +77,7 @@ export default (source: AnyConstructor<any>): Thunk<GraphQLFieldConfigMap<any, a
const merged = mapValues<FieldConfigMap<any, any>, FieldConfig<any, any, any>>({
...allMaps,
...allFields,
}, flow(addDefaultResolver, wrapResolver));
}, (value, key) => wrapResolver(addDefaultResolver(value, key)));

return buildFieldConfigMap(merged);
};
Expand Down
31 changes: 3 additions & 28 deletions src/decorators/__tests__/types.test.ts
@@ -1,39 +1,14 @@
import 'jest';
import { exec } from 'child_process';
import { readdir } from 'fs';
import pify from 'pify';
import path from 'path';

// Would be better to do this programmatically, didn't look straightforward though.
const typeCheckFiles = async (files: string[], expectError?: boolean) => {
await Promise.all(files.map(file => new Promise((resolve, reject) => {
const tscArgs = '--strict --experimentalDecorators --emitDecoratorMetadata --noEmit --lib esnext';
exec(`$(npm bin)/tsc ${tscArgs} ${file}`, (err) => {
if ((err && expectError) || (!err && !expectError)) {
resolve();
} else {
reject(new Error(expectError
? 'Types valid, expected invalid'
: 'Types invalid, expected valid',
));
}
});
})));
};

const getFiles = async (directory: string): Promise<string[]> => {
const files = await pify(readdir)(path.join(__dirname, directory));
return files.map((file: string) => path.join(__dirname, directory, file));
}
import { getFilesInDir, typeCheckFiles } from '../../../typeChecking';

describe('Decorator type validation', () => {
it('passes for valid files', async () => {
const validFiles = await getFiles('valid');
const validFiles = await getFilesInDir(__dirname, 'valid');
await typeCheckFiles(validFiles);
}, 30000);

it('fails for invalid files', async () => {
const invalidFiles = await getFiles('invalid');
const invalidFiles = await getFilesInDir(__dirname, 'invalid');
await typeCheckFiles(invalidFiles, true);
}, 30000);
});
85 changes: 29 additions & 56 deletions src/typeHelpers.ts
Expand Up @@ -4,40 +4,23 @@ import {
GraphQLNamedType,
GraphQLNonNull,
GraphQLOutputType,
GraphQLType,
GraphQLType, isInputType, isNamedType, isOutputType,
} from 'graphql';
import { isInputObjectType, isInterfaceType, isObjectType } from './metadata';
import getInputObjectType from './builders/getInputObjectType';
import getObjectType from './builders/getObjectType';
import getInterfaceType from './builders/getInterfaceType';

export const getNamedType = (target: WrapperOrType<any, GraphQLNamedType>): GraphQLNamedType => {
import { Constructor } from './types';

export function getType(target: WrapperOrType<any, GraphQLType>, nonNull?: false): GraphQLType;
export function getType(target: WrapperOrType<any, GraphQLType>, nonNull: true): GraphQLNonNull<GraphQLType>;
export function getType(target: WrapperOrType<any, GraphQLType>, nonNull?: boolean): GraphQLType | GraphQLNonNull<GraphQLType>;
export function getType(
target: WrapperOrType<any, GraphQLType>,
nonNull?: boolean,
): GraphQLType | GraphQLNonNull<GraphQLType> {
if (isWrapper(target)) {
return resolveWrapper(target, true);
}

if (isInputObjectType(target)) {
return getInputObjectType(target);
}

if (isObjectType(target)) {
return getObjectType(target);
}

if (isInterfaceType(target)) {
return getInterfaceType(target);
}

throw new Error(`Named type not found for ${target.name}`);
}

export const getNamedTypes = (targets: Array<WrapperOrType<any, GraphQLNamedType>>): GraphQLNamedType[] => {
return targets.map(getNamedType);
}

export function getType(target: WrapperOrType<any, GraphQLType>, nonNull?: boolean): GraphQLType {
if (isWrapper(target)) {
return resolveWrapper(target);
return resolveWrapper(target, nonNull);
}

let type;
Expand All @@ -60,40 +43,30 @@ export function getType(target: WrapperOrType<any, GraphQLType>, nonNull?: boole
throw new Error(`Type not found for ${target.name}`);
}

export const getOutputType = (target: WrapperOrType<any, GraphQLOutputType>, nonNull?: boolean): GraphQLOutputType => {
if (isWrapper(target)) {
return resolveWrapper(target);
}

let type;
if (isObjectType(target)) {
type = getObjectType(target);
export const getNamedType = (target: WrapperOrType<any, GraphQLNamedType>): GraphQLNamedType => {
const type = getType(target);
if (!type || !isNamedType(type)) {
throw new Error(`Named type not found for ${(target as Constructor<any>).name}`);
}
return type;
}

if (isInterfaceType(target)) {
type = getInterfaceType(target);
}
export const getNamedTypes = (targets: Array<WrapperOrType<any, GraphQLNamedType>>): GraphQLNamedType[] => {
return targets.map(getNamedType);
}

if (type) {
return nonNull ? new GraphQLNonNull(type) : type;
export const getOutputType = (target: WrapperOrType<any, GraphQLOutputType>, nonNull?: boolean): GraphQLOutputType => {
const type = getType(target, nonNull);
if (!type || !isOutputType(type)) {
throw new Error(`Output type not found for ${(target as Constructor<any>).name}`);
}

throw new Error(`Output type not found for ${target.name}`);
return type;
}

export const getInputType = (target: WrapperOrType<any, GraphQLInputType>, nonNull?: boolean): GraphQLInputType => {
if (isWrapper(target)) {
return resolveWrapper(target);
}

let type;
if (isInputObjectType(target)) {
type = getInputObjectType(target);
}

if (type) {
return nonNull ? new GraphQLNonNull(type) : type;
const type = getType(target, nonNull);
if (!type || !isInputType(type)) {
throw new Error(`Input type not found for ${(target as Constructor<any>).name}`);
}

throw new Error(`Input type not found for ${target.name}`);
return type;
}

0 comments on commit f8cd7e4

Please sign in to comment.