Skip to content

Commit

Permalink
feat(relationships): adds OneToMany and ManyToOne (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
goldcaddy77 authored Jan 12, 2019
1 parent bf09179 commit 3fb97f1
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 20 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && yarn test",
"pre-commit": "lint-staged && tsc && yarn test",
"pre-version": "commitlint -E HUSKY_GIT_PARAMS"
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ export class App {
}

async start() {
await this.writeGeneratedIndexFile();
await this.establishDBConnection();
await this.generateTypes();
await this.buildGraphQLSchema();
await this.writeSchemaFile();
await this.writeGeneratedIndexFile();
await this.generateBinding();

this.graphQLServer = new ApolloServer({
Expand Down
4 changes: 2 additions & 2 deletions src/decorators/EnumField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ export function EnumField(name: string, enumeration: object, options: EnumFieldO

// 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 enumFileName = caller();

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

const factories = [
Expand Down
35 changes: 35 additions & 0 deletions src/decorators/ManyToOne.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'reflect-metadata';

import { Field } from 'type-graphql';
import { JoinColumn, ManyToOne as TypeORMManyToOne } from 'typeorm';

import { StringField } from '../decorators';
import { composeMethodDecorators, MethodDecoratorFactory } from '../utils';

export function ManyToOne(parentType: any, joinFunc: any, options: any = {}): any {
// Need to grab the class name from within a decorator
let klass: string;
const extractClassName = (target: any): any => {
klass = target.constructor.name;
};

// This Decorator creates the foreign key field for the association so that the consumer
// Doesn't need to hand roll this each time by doing somethign like:
// @StringField()
// userId?: ID;
const createForeignKeyField = (target: any, propertyKey: string, descriptor: PropertyDescriptor): any => {
klass = target.constructor.name;
Reflect.defineProperty(target, `${klass}Id`, {});
StringField()(target, `${propertyKey}Id`, descriptor);
};

const factories: MethodDecoratorFactory[] = [
Field(parentType, { nullable: true, ...options }) as MethodDecoratorFactory,
TypeORMManyToOne(parentType, joinFunc, options) as MethodDecoratorFactory,
JoinColumn() as MethodDecoratorFactory,
createForeignKeyField,
extractClassName // Note: this is actually run first because of now composeMethodDecorators works
];

return composeMethodDecorators(...factories);
}
15 changes: 14 additions & 1 deletion src/decorators/Model.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
const caller = require('caller');
import { ObjectType } from 'type-graphql';
import { Entity } from 'typeorm';

import { getMetadataStorage } from '../metadata';
import { composeClassDecorators, ClassDecoratorFactory } from '../utils/';

interface ModelOptions {
auditTableName?: string;
}

export function Model(this: any, args: ModelOptions = {}): any {
const factories = [Entity() as ClassDecoratorFactory, ObjectType() as ClassDecoratorFactory];
const modelFileName = caller();

const registerModelWithWarthog = (target: any): any => {
// Save off where the model is located so that we can import it in the generated classes
getMetadataStorage().addModel(target.name, target, modelFileName);
};

const factories = [
Entity() as ClassDecoratorFactory,
ObjectType() as ClassDecoratorFactory,
registerModelWithWarthog as ClassDecoratorFactory
];

return composeClassDecorators(...factories);
}
13 changes: 13 additions & 0 deletions src/decorators/OneToMany.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Field } from 'type-graphql';
import { OneToMany as TypeORMOneToMany } from 'typeorm';

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

export function OneToMany(parentType: any, joinFunc: any, options: any = {}): any {
const factories = [
Field(parentType, { nullable: true, ...options }) as MethodDecoratorFactory,
TypeORMOneToMany(parentType, joinFunc) as MethodDecoratorFactory
];

return composeMethodDecorators(...factories);
}
10 changes: 6 additions & 4 deletions src/decorators/StringField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ export function StringField(args: StringFieldOptions = {}): any {

// These are the 2 required decorators to get type-graphql and typeorm working
const factories = [
Field({ ...nullableOption }),
// We explicitly say string here because when we're metaprogramming without
// TS types, Field does not know that this should be a String
Field(type => String, {
...nullableOption
}),
Column({
type: 'varchar',
...maxLenOption,
...nullableOption,
...uniqueOption
}) as MethodDecoratorFactory
];

// if (!args.nullable) {
// factories.push(IsDefined());
// }
if (args.minLength) {
factories.push(MinLength(args.minLength));
}
Expand Down
2 changes: 2 additions & 0 deletions src/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from './EnumField';
export * from './Model';
export * from './StringField';
export * from './ForeignKeyField';
export * from './ManyToOne';
export * from './OneToMany';
16 changes: 16 additions & 0 deletions src/metadata/metadata-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function getMetadataStorage(): MetadataStorage {

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

addEnum(tableName: string, columnName: string, enumName: string, enumValues: any, filename: string) {
this.enumMap[tableName] = this.enumMap[tableName] || {};
Expand All @@ -17,10 +18,25 @@ export class MetadataStorage {
};
}

addModel(name: string, klass: any, filename: string) {
this.classMap[name] = {
name,
klass,
filename
};
}

getEnum(tableName: string, columnName: string) {
if (!this.enumMap[tableName]) {
return undefined;
}
return this.enumMap[tableName][columnName] || undefined;
}

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

import {
entityListToEnumImports,
entityListToImports,
entityToOrderByEnum,
entityToWhereArgs,
entityToWhereInput,
Expand All @@ -28,7 +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('')}
${entityListToImports(entities).join('')}

This comment has been minimized.

Copy link
@deweyjose

deweyjose Jan 23, 2019

Contributor

I think this change is causing TS6133 errors (declared/unused).

`;

entities.forEach((entity: EntityMetadata) => {
Expand Down
27 changes: 18 additions & 9 deletions src/schema/TypeORMConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,28 @@ function uniquesForEntity(entity: EntityMetadata): string[] {
);
}

export function entityListToEnumImports(entities: EntityMetadata[]): string[] {
let enums: string[] = [];
export function entityListToImports(entities: EntityMetadata[]): string[] {
let imports: string[] = [];
let enumMap = getMetadataStorage().enumMap;

Object.keys(enumMap).forEach((tableName: string) => {
Object.keys(enumMap[tableName]).forEach((columnName: string) => {
const enumColumn = enumMap[tableName][columnName];
const filename = enumColumn.filename.replace(/\.(j|t)s$/, '');
enums.push(`
import { ${enumColumn.name} } from '${filename}'
`);
imports.push(`import { ${enumColumn.name} } from '${filename}'
`);
});
});

return enums;
let classMap = getMetadataStorage().classMap;
Object.keys(classMap).forEach((tableName: string) => {
const classObj = classMap[tableName];
const filename = classObj.filename.replace(/\.(j|t)s$/, '');
imports.push(`import { ${classObj.name} } from '${filename}'
`);
});

return imports;
}

export function entityToWhereUniqueInput(entity: EntityMetadata): string {
Expand Down Expand Up @@ -90,11 +97,13 @@ export function entityToCreateInput(entity: EntityMetadata): string {

if (column.enum) {
fieldTemplates += `
@Field(type => ${graphQLType}, ${nullable}) ${column.propertyName}${tsRequired}: ${graphQLType};
@Field(type => ${graphQLType}, ${nullable})
${column.propertyName}${tsRequired}: ${graphQLType};
`;
} else {
fieldTemplates += `
@Field(${nullable}) ${column.propertyName}${tsRequired}: ${tsType};
@Field(${nullable})
${column.propertyName}${tsRequired}: ${tsType};
`;
}
}
Expand Down Expand Up @@ -333,8 +342,8 @@ export function columnToGraphQLType(column: ColumnMetadata): GraphQLScalarType |
switch (type) {
case String:
case 'String':
case 'varchar':
case 'text':
case 'enum': // TODO: Hack for now, need to teach this about enums
return GraphQLString;
case Boolean:
case 'Boolean':
Expand Down

0 comments on commit 3fb97f1

Please sign in to comment.