Skip to content

Commit

Permalink
feat(enums): adds enum support (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
goldcaddy77 authored Jan 12, 2019
1 parent 8179135 commit 7ec77b7
Show file tree
Hide file tree
Showing 19 changed files with 280 additions and 31 deletions.
1 change: 1 addition & 0 deletions examples/1-simple-seed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"start:ts": "ts-node --type-check src/index.ts",
"typeorm:cli": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -f ./ormconfig",
"test": "dotenv -- jest",
"test:watch": "dotenv -- jest --watch",
"watch:ts": "nodemon -e ts,graphql -x ts-node --type-check src/index.ts"
},
"dependencies": {
Expand Down
5 changes: 2 additions & 3 deletions examples/1-simple-seed/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('Users', () => {
});

test('uniqueness failure', async done => {
let error;
let error: GraphQLError = new GraphQLError('');
try {
await binding.mutation.createUser(
{
Expand All @@ -74,9 +74,8 @@ describe('Users', () => {
`{ id email createdAt createdById }`
);
} catch (e) {
error = e;
error = e as GraphQLError;
}

// Note: this test can also surface if you have 2 separate versions of GraphQL installed (which is bad)
expect(error).toBeInstanceOf(GraphQLError);
expect(error.message).toContain('duplicate');
Expand Down
2 changes: 2 additions & 0 deletions examples/1-simple-seed/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'reflect-metadata';
import * as dotenv from 'dotenv';
dotenv.config();

import { getApp } from './app';

Expand Down
12 changes: 11 additions & 1 deletion examples/1-simple-seed/src/modules/user/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Authorized } from 'type-graphql';

import { BaseModel, EmailField, Model, StringField } from '../../../../../src';
import { BaseModel, EmailField, EnumField, Model, StringField } from '../../../../../src';

// Note: this must be exported and in the same file where it's attached with @EnumField
// Also - must use string enums
export enum StringEnum {
FOO = 'FOO',
BAR = 'BAR'
}

@Model()
export class User extends BaseModel {
Expand All @@ -10,6 +17,9 @@ export class User extends BaseModel {
@StringField({ maxLength: 50, minLength: 2 })
lastName?: string;

@EnumField('StringEnum', StringEnum, { nullable: true })
stringEnumField?: StringEnum;

@EmailField()
email?: string;

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"build": "yarn tsc",
"semantic-release": "semantic-release",
"test": "jest --verbose --coverage",
"test:watch": "jest --verbose --watch",
"test:ci": "jest --coverage --ci --forceExit --detectOpenHandles --runInBand"
},
"husky": {
Expand All @@ -36,6 +37,7 @@
"homepage": "https://github.com/goldcaddy77/warthog#readme",
"//": "TODO: figure out which of these are dependencies, devDeps or peerDeps",
"dependencies": {
"@types/caller": "^1.0.0",
"@types/debug": "^0.0.31",
"@types/dotenv": "^6.1.0",
"@types/express": "^4.16.0",
Expand All @@ -50,9 +52,11 @@
"@types/prettier": "^1.15.2",
"@types/shortid": "^0.0.29",
"@types/ws": "^6.0.1",
"apollo-link-error": "^1.1.5",
"apollo-link-http": "^1.5.9",
"apollo-server": "^2.3.1",
"apollo-server-express": "^2.3.1",
"caller": "^1.0.1",
"class-transformer": "^0.2.0",
"class-validator": "^0.9.1",
"cross-fetch": "^3.0.0",
Expand Down
6 changes: 1 addition & 5 deletions src/core/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,7 @@ export class App {
};
return context;
},
schema: this.schema,
formatError: (error: Error) => {
console.log(error);
return error;
}
schema: this.schema
});

const app = express();
Expand Down
15 changes: 12 additions & 3 deletions src/core/binding.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { onError } from 'apollo-link-error';
import { HttpLink } from 'apollo-link-http';
import * as fetch from 'cross-fetch';
import * as fs from 'fs';
import * as Debug from 'debug';
import { buildSchema, printSchema } from 'graphql';
import { buildSchema, GraphQLError, printSchema } from 'graphql';
import { Binding, TypescriptGenerator } from 'graphql-binding';
import { introspectSchema, makeRemoteExecutableSchema } from 'graphql-tools';
import * as path from 'path';
Expand Down Expand Up @@ -39,8 +40,16 @@ export class Link extends HttpLink {
}

export class RemoteBinding extends Binding {
constructor(link: HttpLink, typeDefs: string) {
const schema = makeRemoteExecutableSchema({ link, schema: typeDefs });
constructor(httpLink: HttpLink, typeDefs: string) {
// Workaround for issue with graphql-tools
// See https://github.com/graphql-binding/graphql-binding/issues/173#issuecomment-446366548
const errorLink = onError((args: any) => {
if (args.graphQLErrors && args.graphQLErrors.length === 1) {
args.response.errors = args.graphQLErrors.concat(new GraphQLError(''));
}
});

const schema = makeRemoteExecutableSchema({ link: errorLink.concat(httpLink), schema: typeDefs });
debug('schema', JSON.stringify(schema));
super({ schema });
}
Expand Down
4 changes: 4 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export type WhereInput = {
export type DeleteReponse = {
id: ID;
};

export interface ClassType<T = any> {
new (...args: any[]): T;
}
49 changes: 49 additions & 0 deletions src/decorators/EnumField.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'reflect-metadata';

import { IntrospectionSchema, IntrospectionEnumType } from 'graphql';
import { ObjectType, Query, Resolver } from 'type-graphql';

import { getSchemaInfo } from '../schema';

import { EnumField } from './EnumField';

describe('Enums', () => {
let schemaIntrospection: IntrospectionSchema;

beforeAll(async () => {
enum StringEnum {
Foo = 'FOO',
Bar = 'BAR'
}

@ObjectType()
class StringEnumInput {
@EnumField('StringEnum', StringEnum, { nullable: true })
stringEnumField?: StringEnum;
}

@Resolver(of => StringEnumInput)
class SampleResolver {
@Query(returns => StringEnum)
getStringEnumValue(): StringEnum {
return StringEnum.Foo;
}
}

const schemaInfo = await getSchemaInfo({
resolvers: [SampleResolver]
});
schemaIntrospection = schemaInfo.schemaIntrospection;
});

describe('EnumField', () => {
it('Puts an enum in the GraphQL schema', async () => {
const myEnum = schemaIntrospection.types.find((type: any) => {
return type.kind === 'ENUM' && type.name === 'StringEnum';
}) as IntrospectionEnumType;

expect(myEnum).toBeDefined();
expect(myEnum.enumValues.length).toEqual(2);
});
});
});
31 changes: 31 additions & 0 deletions src/decorators/EnumField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const caller = require('caller');
import { Field, registerEnumType } from 'type-graphql';
import { Column } from 'typeorm';

import { getMetadataStorage } from '../metadata';
import { composeMethodDecorators, MethodDecoratorFactory } from '../utils';

interface EnumFieldOptions {
nullable?: boolean;
}

export function EnumField(name: string, enumeration: object, options: EnumFieldOptions = {}): any {
// Register enum with TypeGraphQL so that it lands in generated schema
registerEnumType(enumeration, { name });

// In order to use the enums in the generated classes file, we need to
// save their locations and import them in the generated file
const entityFileName = caller();

const registerEnumWithWarthog = (target: any, propertyKey: string, descriptor: PropertyDescriptor): any => {
getMetadataStorage().addEnum(target.constructor.name, propertyKey, name, enumeration, entityFileName);
};

const factories = [
Field(type => enumeration, options),
Column({ enum: enumeration, ...options }) as MethodDecoratorFactory,
registerEnumWithWarthog
];

return composeMethodDecorators(...factories);
}
1 change: 1 addition & 0 deletions src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './EmailField';
export * from './EnumField';
export * from './Model';
export * from './StringField';
export * from './ForeignKeyField';
1 change: 1 addition & 0 deletions src/metadata/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getMetadataStorage } from './metadata-storage';
26 changes: 26 additions & 0 deletions src/metadata/metadata-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function getMetadataStorage(): MetadataStorage {
if (!(global as any).WarthogMetadataStorage) {
(global as any).WarthogMetadataStorage = new MetadataStorage();
}
return (global as any).WarthogMetadataStorage;
}

export class MetadataStorage {
enumMap: { [table: string]: { [column: string]: any } } = {};

addEnum(tableName: string, columnName: string, enumName: string, enumValues: any, filename: string) {
this.enumMap[tableName] = this.enumMap[tableName] || {};
this.enumMap[tableName][columnName] = {
name: enumName,
enumeration: enumValues,
filename
};
}

getEnum(tableName: string, columnName: string) {
if (!this.enumMap[tableName]) {
return undefined;
}
return this.enumMap[tableName][columnName] || undefined;
}
}
2 changes: 2 additions & 0 deletions src/schema/SchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as prettier from 'prettier';
import { EntityMetadata } from 'typeorm';

import {
entityListToEnumImports,
entityToOrderByEnum,
entityToWhereArgs,
entityToWhereInput,
Expand All @@ -27,6 +28,7 @@ export class SchemaGenerator {
import { ArgsType, Field, InputType } from 'type-graphql';
import { registerEnumType } from 'type-graphql';
import { BaseWhereInput, PaginationArgs } from '${warthogImportPath}';
${entityListToEnumImports(entities).join('')}
`;

entities.forEach((entity: EntityMetadata) => {
Expand Down
Loading

0 comments on commit 7ec77b7

Please sign in to comment.