goldcaddy77 / warthog

GraphQL API Framework with strong conventions and auto-generated schema
https://warthog.dev/
MIT License
359 stars 38 forks source link

feat: make Single Table Inheritance usable #371

Open diegonc opened 4 years ago

diegonc commented 4 years ago

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 :-)

diegonc commented 4 years ago

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

goldcaddy77 commented 4 years ago

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.

goldcaddy77 commented 3 years ago

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

diegonc commented 3 years ago

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

goldcaddy77 commented 3 years ago

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.

diegonc commented 3 years ago

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>)
goldcaddy77 commented 3 years ago

Can you shoot me a link to your branch?

diegonc commented 3 years ago

This is what I have so far:

https://github.com/diegonc/warthog/tree/t/single-table-inheritance

goldcaddy77 commented 3 years ago

Note to self to see diff: https://github.com/goldcaddy77/warthog/compare/main...diegonc:t/single-table-inheritance