goldcaddy77/warthog

feat: make Single Table Inheritance usable

diegonc opened this issue · 9 comments

TypeORM has a mode in which entities in a tree may share the same table as it's ancestor.
It goes something like in the following snippets:

export enum PartyType {
  PtyPerson = "Person",
  PtyOrganization = "Organization",
  PtyGroup = "Group"
}

@Entity()
@TableInheritance({column: {type: 'varchar', name: 'type'}})
export class Party extends BaseModel { ... }

@ChildEntity(PartyType.PtyPerson)
export class Person extends Party { ... }

@ChildEntity(PartyType.PtyGroup)
export class Group extends Party { ... }

Class party can already be implemented by just using the TableInheritance decorator from TypeORM along with Model from warthog.

Child classes however cannot be added as warthog's Model decorator uses Entity. So, I came up with a custom decorator sitting in my code tree which I called ChildModel and is shown below.

const caller = require('caller'); // eslint-disable-line @typescript-eslint/no-var-requires
import * as path from 'path';
import { ObjectType } from 'type-graphql';
import { ObjectOptions } from 'type-graphql/dist/decorators/ObjectType.d';
import { Container } from 'typedi';
import { ChildEntity } from 'typeorm';
import {
  ClassDecoratorFactory,
  ClassType,
  composeClassDecorators,
  generatedFolderPath,
} from 'warthog';

function getMetadataStorage(): any {
  if (!(global as any).WarthogMetadataStorage) {
    // Since we can't use DI to inject this, just call into the container directly
    (global as any).WarthogMetadataStorage = Container.get('MetadataStorage');
  }
  return (global as any).WarthogMetadataStorage;
}

interface ChildModelOptions {
  api?: ObjectOptions;
}

// Allow default TypeORM and TypeGraphQL options to be used
export function ChildModel(discriminatorValue?: any, { api = {} }: ChildModelOptions = {}) {
  // 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 modelFileName = caller();

  // Use relative paths when linking source files so that we can check the generated code in
  // and it will work in any directory structure
  const relativeFilePath = path.relative(generatedFolderPath(), modelFileName);

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

  const factories = [
    ChildEntity(discriminatorValue) as ClassDecoratorFactory,
    ObjectType(api) as ClassDecoratorFactory,
    registerModelWithWarthog as ClassDecoratorFactory
  ];

  return composeClassDecorators(...factories);
}

Maybe it's a good idea to add the decorator to warthog's source tree :-)

Oh, I just read in typeorm docs it's an experimental feature yet. Maybe it's not as good idea as I thought.

Hey, thanks for the code sample. Do you want to put it in as a PR in the examples folder? Then it’s not “officially supported” but folks have the pattern handy.

Hey @diegonc . I'd still be interested in getting this represented in the examples folder if you're interested in adding it.

Hi, where should I put it? Do I just pick the next number and create a new folder?

Yeah, that works. I typically just copy the last folder, re-number the DB name, etc... and gut the model, service and resolver of what's not needed.

Sorry I cannot seem to make it work like it did at that time. Even my original project looks broken now. I cannot make Person and Group implement Party at the graphql schema.

Extending from Party doesn't add the implements clause and adding {api: {implements: Party}} to group or person gives an error (I suspect due to the circular dependencies):

$ warthog codegen
TypeError: Cannot read property 'type' of undefined
    at /home/diegonc/dev/test/quasar/wireframe-api/node_modules/type-graphql/dist/schema/schema-generator.js:157:149
    at Array.map (<anonymous>)
    at interfaces (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/type-graphql/dist/schema/schema-generator.js:157:59)
    at resolveThunk (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/graphql/type/definition.js:438:40)
    at defineInterfaces (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/graphql/type/definition.js:619:20)
    at GraphQLObjectType.getInterfaces (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/graphql/type/definition.js:587:31)
    at typeMapReducer (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/graphql/type/schema.js:276:28)
    at typeMapReducer (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/graphql/type/schema.js:286:20)
    at typeMapReducer (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/graphql/type/schema.js:286:20)
    at Array.reduce (<anonymous>)
    at new GraphQLSchema (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/graphql/type/schema.js:145:28)
    at Function.generateFromMetadataSync (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/type-graphql/dist/schema/schema-generator.js:31:24)
    at Function.<anonymous> (/home/diegonc/dev/test/quasar/wireframe-api/node_modules/type-graphql/dist/schema/schema-generator.js:16:33)
    at Generator.next (<anonymous>)
    at /home/diegonc/dev/test/quasar/wireframe-api/node_modules/tslib/tslib.js:115:75
    at new Promise (<anonymous>)

Can you shoot me a link to your branch?