vesper-framework / vesper

Vesper is a NodeJS framework that helps you to create scalable, maintainable, extensible, declarative and fast GraphQL-based server applications.
http://vesper-framework.com
600 stars 35 forks source link

[Feature Request] GraphQL interfaces #20

Open nierennakker opened 6 years ago

nierennakker commented 6 years ago

First of all, thanks for this great framework!

I was wondering if I could use GraphQL interfaces with Vesper. I did some source digging, but I couldn't find anything about it. Also, when I run my server with an interface, a warning gets printed to the console:

Type "..." is missing a "resolveType" resolver. Pass false into "resolverValidationOptions.requireResolversForResolveType" to disable this warning.
pleerock commented 6 years ago

please provide a proposal how do you see it implemented in vesper and lets see if we can do that

nierennakker commented 6 years ago

It should be possible to use the discriminator column to determine the type of the interface. Using that, writing a custom resolveType function shouldn't be needed.

interface Test {
  id: ID!
  var: String!
}

type First implements Test {
  id: ID!
  var: String!
  first: Int!
}

type Second implements Test {
  id: ID!
  var: String!
  second: String!
}
@Entity()
@TableInheritance({ column: 'type' })
@TypeResolver('type' /* or { name: 'Test', column: 'type' } */)
class Test {
  @PrimaryGeneratedColumn()
  public id!: string;

  @Column()
  public var!: string;

  @Column()
  @DiscriminatorType({ first: 'First', second: 'Second' })
  public type!: string; // internally matches the value of this column
}

@ChildEntity()
class First extends Test {
  @Column()
  public first!: number;
}

@ChildEntity()
class Second extends Test {
  @Column()
  public second!: string;
}
josephktcheung commented 6 years ago

I want to know how to query interface result as well. for example we have:

interface Test {
  id: ID!
  var: String!
}

type First implements Test {
  id: ID!
  var: String!
  first: Int!
}

type Second implements Test {
  id: ID!
  var: String!
  second: String!
}

And we have our table like this:

@Entity()
@TableInheritance({ column: 'type' })
@TypeResolver('type' /* or { name: 'Test', column: 'type' } */)
class Test {
  @PrimaryGeneratedColumn()
  public id!: string;

  @Column()
  public var!: string;

  @Column()
  @DiscriminatorType({ first: 'First', second: 'Second' })
  public type!: string; // internally matches the value of this column
}

@ChildEntity()
class First extends Test {
  @Column()
  public first!: number;
}

@ChildEntity()
class Second extends Test {
  @Column()
  public second!: string;
}

and we have query like this:

type Query {
  tests: [Test]
}

Which supposes to return all records from First and Second, can vesper help making a union query? Joinmonster has instruction on how to do union / interface query https://join-monster.readthedocs.io/en/latest/unions/, we may be able to do something similar.

vfaramond commented 6 years ago

I'm using GraphQL interfaces along with TypeORM single table inheritance with Vesper. You just need to provide the resolveType for the GraphQL interface, like this in your case:

    const schema = await buildVesperSchema({
      ...
      customResolvers: {
        Test: {
          __resolveType: data => data.constructor.name,
        },
      },
      ...
    });

You'll now be able to use your query like this:

{
  tests {
    id
    var
    ... on First {
      first
    }
    ... on Second {
      second
    }
  }
}
josephktcheung commented 6 years ago

Yeah @vfaramond, that's what I did in our projects too. Resolving interfaces is easy, but querying multiple entities which implements the interface at the same time can be tricky. I need to write custom sql query to union the result myself.

e.g. I have 2 entities called Foo and Bar which implements Baz, which is an abstract class

// Baz.ts
abstract class Baz {
  @PrimaryGeneratedColumn() id: string;
  @Column() baz: string;
}
// Foo.ts
class Foo extends Baz {
  @Column() foo: string;
}
// Bar.ts
class Bar extends Baz {
  @Column() bar: string;
}

And I want to query both Foo and Bar at the same time using a query called bazs and I can filter them based on their common field baz

interface Baz {
  baz: String
}
type Foo implements Baz {
  baz: String
  foo: String
}
type Bar implements Baz {
  baz: String
  foo: String
}
type Query {
  bazs(baz: String): [Baz]
}

When I resolve bazs query, what I do atm is to return union result:

// BazController.ts
@Controller()
export class BazController {
  constructor(private readonly entityManager: EntityManager) {}
  @Query()
  async bazs(args: BazsArgs) {
    const query = `
      select id, baz, foo, NULL as bar from foo where foo.baz = $1
      union 
      select id, baz, bar, NULL as foo from bar where bar.baz = $1`;
    return this.entityManger.query(query, [args.baz]);
  }
}

Because Vesper has automatic database relations resolver, I wonder if it can take 1 step further to help generate the interface query for us instead of us writing it ourselves.

Interface type and union type features are discussed at length on different use cases in other graphql server frameworks e.g. Prisma https://github.com/prismagraphql/prisma/issues/83, https://github.com/prismagraphql/prisma/issues/165. As an alternative to Prisma, I think it's good to at least know what's Vesper's plan on supporting these types.