Lighthouse: [Docs] How to get a paginator using different models? @union or @interface directive

Created on 24 May 2018  路  60Comments  路  Source: nuwave/lighthouse

Hi all,

I'm currently experimenting with the following use case, which seems like a perfect task for the @union directive.

I have an Image and a Slogan model that have very similar properties and I need to have a mixed list exposed by graphql, combining the two types into one list. This should be a paginator from the Relay perspective.

From what I can read in the docs and unit tests, I'm not totally clear on how to go about the above situation. Eg. I don't want to specify a type as a requirement since I want to have both kind of results mixed in a single result set.

Maybe the @interface directive is the way to go? Any pointers in the right direction are greatly appreciated ;-)

Kind regards,

Erik

question

Most helpful comment

Hey @4levels, I think some docs would probably help clear this issue up a bit which I've been lagging behind on and that's my fault. But hopefully I can get a faqs section up along w/ some additional scenarios about when to use certain types/directives sometime soon!

I think you may be confusing a SQL union statement w/ a GraphQL union type. Check out this section of the GraphQL docs which briefly explains how a union works (looks like their SSL cert has expired today btw). But basically, it's a way to say that the results may be one of the specified types, and a search query is a great example of this.

Another great example (and the one I use most frequently) is Polymorphic relationships. Using the Laravel example, a Comment would have a commentable field that could be a Post or a Video like so:

type Post {
  title: String
  body: String
}

type Video {
  title: String
  url: String
}

union Commentable @union(resolver: "App\\GraphQL\\UnionResolver@commentable") = Post | Video

type Comment {
  id: ID!
  message: String
  commentable: Commentable @belongsTo
}

And the resolver would look something like this:

class UnionResolver
{
    public function commentable($value)
    {
        return $value instanceof Post 
            ? schema()->instance('Post') 
            : schema()->instance('Video');
    }
}

Hopefully that helps! As for interfaces, they're pretty interchangeable w/ unions and it seems more of a personal preference when to use them, but if I think of a use-case I'll add it here!

All 60 comments

Just a fair warning ahead: when working with Eloquent unions, there's an old (as in 2 years old) issue in laravel that prevents pagination from working correctly out of the box.

See https://github.com/laravel/framework/issues/14837 for a solution, seems we might need to work with a fork of Laravel since the proposed solution never made it in :disappointed:

Ok, after stumbling on the forementioned laravel issue, this is how far I've come in my efforts to make a combined list of both models with pagination. It currently still returns only one type (Slogan) and I'm sure I'm doing quite some things wrong here.
I think a dataloader might be the solution?
The @rename attribute is currently not working either (just like any of the other uncommon fields) but I think there's a perfect explanation for this since I've just been experimenting..

  • created a new model Works in app/Models/ without extending the Eloquent model class
<?php
namespace App\Models;
class Work {
    public function resolve($root, $args = null) {
        return schema()->instance('image' == $args['type']
            ? 'Image'
            : 'Slogan'
        );
    }
    public function work($value)
    {
        $type = isset($value['id']) ? 'Image' : 'Slogan';
        return schema()->instance($type);
    }
    public static function query() {
        $images = Image::query()
            ->select('id', 'filename AS data', 'created_at')
        ;
        $works = Slogan::query()
            ->select('id', 'slogan AS data', 'created_at')
            ->unionAll($images)
            ->orderBy('created_at', 'desc')
        ;
        return $works;
    }
}
  • updated schema.graphql:
type Image @model {
  id: ID! @globalId
  filename: String
  data: String @rename(attribute: "filename")
  created_at: DateTime
}
type Slogan {
  id: ID! @globalId
  slogan: String
  data: String @rename(attribute: "slogan")
  created_at: DateTime
}
union Work @union(resolver: "App\\Models\\Work@resolve") = Image | Slogan
type User {
  id: ID! @globalId
  email: String!
  ...
  works (type: String): [Work!] @paginate(type: "relay", model: "Work")
}

Running the following query (with fragments) seems to work but I'm only getting Slogans

{ 
  viewer {
    id
    email
    works (first: 2) {
      edges {
        node {
          __typename
          ...WorkDetail_image
          ...WorkDetail_slogan
        }
      }
    }
}
fragment WorkDetail_image on Image {
  id
  data
  created_at
} 
fragment WorkDetail_slogan on Slogan {
  id
  data
  created_at
}

yields the following results:

{
  "data": {
    "viewer": {
      "email": "[email protected]",
      "works": {
        "edges": [
          {
            "node": {
              "__typename": "Slogan",
              "id": "U2xvZ2FuOjk4MzM=",
              "data": null,
              "created_at": "2018-03-19T20:15:26+01:00"
            }
          },
          {
            "node": {
              "__typename": "Slogan",
              "id": "U2xvZ2FuOjk4MzI=",
              "data": null,
              "created_at": "2018-02-26T11:41:01+01:00"
            }
          }
        ]
      }
    }
  }
}

This is very similar from a recent ussue from Alex black.

Try againg with a recent commit!

Hi @kikoseijo

I just pulled the latest branch, but since my approach is pbbly all wrong, I'm getting identical results
Do you have experience with the @union directive?

Thanks for the quick reply!

Erik

Hey @4levels, I think some docs would probably help clear this issue up a bit which I've been lagging behind on and that's my fault. But hopefully I can get a faqs section up along w/ some additional scenarios about when to use certain types/directives sometime soon!

I think you may be confusing a SQL union statement w/ a GraphQL union type. Check out this section of the GraphQL docs which briefly explains how a union works (looks like their SSL cert has expired today btw). But basically, it's a way to say that the results may be one of the specified types, and a search query is a great example of this.

Another great example (and the one I use most frequently) is Polymorphic relationships. Using the Laravel example, a Comment would have a commentable field that could be a Post or a Video like so:

type Post {
  title: String
  body: String
}

type Video {
  title: String
  url: String
}

union Commentable @union(resolver: "App\\GraphQL\\UnionResolver@commentable") = Post | Video

type Comment {
  id: ID!
  message: String
  commentable: Commentable @belongsTo
}

And the resolver would look something like this:

class UnionResolver
{
    public function commentable($value)
    {
        return $value instanceof Post 
            ? schema()->instance('Post') 
            : schema()->instance('Video');
    }
}

Hopefully that helps! As for interfaces, they're pretty interchangeable w/ unions and it seems more of a personal preference when to use them, but if I think of a use-case I'll add it here!

Hi @chrissm79,

thank you for your detailed answer, I'll be getting back to trying this asap!

As a workaround (to get things going) I ended up with adding a new model Submission that was supposed to encapsulate both Images and Slogans using PHP's inheritance. I tought why bother GraphQL with this if I can handle this on the PHP level. This however was way more challenging than expected since the query builder kept failing because there's no real submission table and mimicing all requirements to make this work seems to daunting atm.

Then I tried with polymorphic relations but since I have one-to-one relations (and not one-to-many), that seems problematic at the moment and there's very little info out there.. It seems like there are assumptions that related models have to be plural but that's ofcourse invalid for one-to-one relations.

Man, from time to time I really dislike Laravel, the documentation often seems to explain one single use case and even Google is not giving me the needed info. Besides that, the issue with Union queries not working for pagination never even got resolved despite a valid PR even existed. In the past I also had a similar experience when I tried to remove the use of Facades out of Passport (a very valid PR that simply got rejected for no good reason at all, with fanboys even attacking me afterwards when I made a remark about it). I mean, aparently Tylor has time to do code formatting updates, but not to add 4 lines of code in the Builder?

So I currently went with the (IMHO dirty) approach to have an actual Submission model, with separate id columns for the relations (ieuw). At least this seems to work and play nice with GraphQL as well..

I'll definitely give it another try using the info you just shared and report back here how I went about it.

Could you also shed some light on the use of subscriptions in #88 ? Seems like I'm not the only one eagerly awaiting some insights on this ;-)

Thanks again!

Erik

Hi @chrissm79,

I tried to verbatim copy your instructions and the ones found in the Laravel documentation, but I'm still not succeeding, neither with the paginator types "relay" or the default. Note I changed the Comment::message property to body to match the Laravel examples. I also adjusted the related model names to read App\Models\Post instead of App\Post
I'm starting to feel cursed or someting, this is really abnormal, I've never struggled this much to get something working that clearly should work!

Models:

  • app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
    public $timestamps = false;
    protected $fillable = [
        'id', 'title', 'body'
    ];
    /**
     * Get all of the post's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Models\Comment', 'commentable');
    }
}
  • app/Models/Video.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Video extends Model
{
    public $timestamps = false;
    protected $fillable = [
        'id', 'title', 'url'
    ];
    /**
     * Get all of the video's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Models\Comment', 'commentable');
    }
}
  • app/Models/Comment.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
    public $timestamps = false;
    protected $fillable = [
        'id', 'body', 'commentable_id', 'commentable_type'
    ];
    /**
     * Get all of the owning commentable models.
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}
  • app/GraphQL/UnionResolver.php
namespace App\GraphQL;
class UnionResolver
{
    public function commentable($value)
    {
        return $value instanceof Post
            ? schema()->instance('Post')
            : schema()->instance('Video');
    }
}
  • app/GraphQL/schema.graphql:
type Post {
  title: String
  body: String
}

type Video {
  title: String
  url: String
}

union Commentable @union(resolver: "App\\GraphQL\\UnionResolver@commentable") = Post | Video

type Comment {
  id: ID!
  body: String
  commentable: Commentable @belongsTo
}

type Query {
  comments: [Comment!]! @paginate(model: "Comment")
}

sample data I added to the database manually:

table posts

id | title | body
-|-|-
1 | Post 1 | Body post 1
2 | Post 2 | Body post 2

table videos

id | title | url
-|-|-
1 | Video 1 | Url video 1
2 | Video 2 | Url video 2

table comments

id | body | commentable_id | commentable_type
-|-|-|-
1 | Comment on Post 1 | 1 | App\Models\Post
2 | Comment on Post 2 | 2 | App\Models\Post
3 | Comment on Video 1 | 1 | App\Models\Video
4 | Comment on Video 2 | 2 | App\Models\Video

Query:

{
  comments (count: 5) {
    data {
      id
      body
      commentable {
        ... on Video {
          title
          url
        }
        ... on Post {
          title
          body
        }        
      }
    }
  }
}

And sadly the results:

{
  "data": {
    "comments": {
      "data": [
        {
          "id": "1",
          "body": "Comment on Post 1",
          "commentable": null
        },
        {
          "id": "2",
          "body": "Comment on Post 2",
          "commentable": null
        },
        {
          "id": "3",
          "body": "Comment on Video 1",
          "commentable": null
        },
        {
          "id": "4",
          "body": "Comment on Video 2",
          "commentable": null
        }
      ]
    }
  }
}

And as a bonus: when I dare to change the comments table to read App\\Models\\Post (double quotes) I get the following PHP Fatal error!

(1/1)聽FatalErrorException
Cannot declare class App\\Models\\Post, because the name is already in use
--
in聽Post.php聽line 23

FYI here's the migration I ran as well:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePolymorphicTables extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {

        // create posts table
        Schema::create('posts', function(Blueprint $table)
        {
            $table->increments('id');
            $table->string('title');
            $table->text('body');
        });

        // create videos table
        Schema::create('videos', function(Blueprint $table)
        {
            $table->increments('id');
            $table->string('title');
            $table->string('url');
        });

        // create comments table
        Schema::create('comments', function(Blueprint $table)
        {
            $table->increments('id');
            $table->text('body');
            $table->unsignedInteger('commentable_id');
            $table->string('commentable_type');
        });
    }
   // down function left out for brevity

Hi @chrissm79, I still have some more questions, but the previous comment was already getting very lengthy...

Is the able suffix required for polymorphic relations?
Why is there no @model directive in schema.grapqhl for Post, Video and Comment?
Obviously I'm mostly interested in the relay implementation, with @globalId and @paginate (type: "relay") since my React Native frontend is depending on that, but the behaviour is identical, whether I add the relay directives or not (even the Fatal error is so kind to stay).

So either Lumen doesn't support polymorphic relations.
Or something down the road is still depending on Facades
Or I'm overlooking something really stupid (typo?)
Or I'm simply doomed, cursed or whatever else supernaturally bad can happen to me.

One more, regarding the resolver:

No matter what I write in there, the commentable function is never called. I can die(), error_log(), etc etc, this function seems never to be reached. I've tried enabling Facades as well, no difference. PHP Fatal error remains as well as soon as I use double quoted values in the comments table, with a pathetic strack trace:

{"exception":"[object] (Symfony\\Component\\Debug\\Exception\\FatalErrorException(code: 64): Cannot declare class App\\Models\\Post, because the name is already in use at /app/Models/Post.php:23)
[stacktrace]
#0 /vendor/laravel/lumen-framework/src/Concerns/RegistersExceptionHandlers.php(54): Laravel\\Lumen\\Application->handleShutdown()
#1 [internal function]: Laravel\\Lumen\\Application->Laravel\\Lumen\\Concerns\\{closure}()
#2 {main}
"}

And why is not simply returning the type, like so?

    public function commentable($value)
    {
        return schema()->instance($value);
    }

@4levels Looks like that was an issue w/ Lighthouse, but luckily it was a quick and easy fix. Update to the latest and give it a go (it tested locally and it now works as expected).

As for passing in the $value, the schema()->instance($value) function expects a string as a parameter to match the name of your GraphQL type.

Hi @chrissm79, that would literally make my day!
Not getting the update though.. (neither does it show in Github - last commit is the merge PR from yesterday)

@4levels sorry about that! didn't realize I got an error because I did pull down the latest changes... give it a try now

WTF, now Passport is throwing fatal errors because it updated from 6.0.0 to 6.0.1
Unbelievable!

Luckily fixing the version to 6.0.0 fixes it, what's in their coffee?

Hi @chrissm79,
You have no idea how much stress is running off me just now after seeing the actual polymorphic models show! Thanks a zillion million times for looking into this so promptly! Voodoo seems over now (besides the Passport hickup), yeah! Finishing up some more stuff here and going to sleep completely relieved (it's 10pm over here).

Oh, and this seems to do the job perfectly and allows for any kind of model class you throw at it :smile: :

    public function commentable($value)
    {
        return schema()->instance(last(explode('\\', get_class($value))));
    }

I'm currently giving this a try in the UnionDirective of Lighthouse, as a fallback if no resolver is passed as argument..

@4levels Looks like there's some sort of issue w/ v6.0.1, weird. I'll be sure to circle back to it later, the project I'm working on uses Passport as well.

That's not a bad idea to resolve the instance, it could do a is_object check and run the code you supplied or fallback to it's current behavior. Feel free to submit a PR for that if you'd like 馃槃

There you go ;-)
PR #130
I'll try to add a testcase also..

Oh, and here's the passport PR that fixes the hickup: https://github.com/laravel/passport/pull/718

Hi @chrissm79,

testing this seems a bit beyond my comprehension as it seems the actual directive is not being used in the test, but a test resolver instead. Not sure how to go about that.

I added a function in the testCase called schemaWithoutResolver, as a copy of the existing schema() function, and tried to use that, but the test keeps failing as soon as I remove the resolve attribute in the @union directive..

At least I can confirm that it works perfectly over here..

Ok, one last question that brings me back to the very beginning of this issue:

How should I go about implementing a mixed list of models that share some properties? Just as seen in the search results example on the GraphQL docs (this is actually an even better scenario!)

Do i NEED an Eloquent model to pull this off? I'm still trying things out a bit, so not in a hurry atm..

Thanks again!

Enough trying ;-) but still no luck :-(

I even tried using the "virtual" model class of jenssegers/model but still I'm failing to use the @union directive since it seems to depend on an actual QueryBuilder..
Even after adding the newQuery and the HasRelationsship trait method returning a query doesn't seem to help.

namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasRelationships;
class Submission extends VirtualModel {
    use HasRelationships;
    protected $fillable = [
        'id', 'searchable_id', 'searchable_type', 'created_at'
    ];
    public function searchable() {
        return $this->morphTo();
    }
    public function newQuery() {
        return Image::query(); // @TODO create an actual union'd query
    }
}

Somehow I feel like I'm _using a cannon to kill a mosquito_ as I'm sure there is a way to simply provide a resolver* or resolve* method in a GraphQL Query class to return a (union'd) query that does the job, resulting in the desired paginatable list of combined models

Going throuhg the Walkthrough video once more..

I've been trying to get things going with the @interface directive instead, but still no luck.
Isn't there anyone out there that can assist, besides @chrissm79 but I guess he's still sleeping atm since it's 2AM there ;-)
cc @kikoseijo maybe?

schema.graphql, currently without pagination in the search query, as this requires a model attribute:

interface Searchable @interface(
    resolver: "App\\GraphQL\\Interfaces\\Searchable@resolveType"
  ) {
  id: ID! @globalId
}
type Image implements Searchable @model {
  id: ID! @globalId
  ...
}
type Slogan implements Searchable @model {
  id: ID! @globalId
  ...
}
type SearchResult {
  id: ID! @globalId
  searchable: Searchable
}
query {
  search: [SearchResult!]!
}

Maybe there's another bug in Lighthouse as the resolveType method below never seems to be called, no matter what I write in there, I just keep getting null as result for searchable.. When error_logging in the InterfaceDirective.php, it seems like it's own resolveType method is never called as well (explaining why my own resolver is never called)

app/GraphQL/Interfaces/Searchable.php

namespace App\GraphQL\Interfaces;

use App\Models\Image;
use App\Models\Slogan;

class Searchable
{
    public function resolveType($value)
    {
        // doesn't matter what I write in here..
        if ($value instanceof Image) {
            return schema()->instance('Image');
        } else if ($value instanceof Slogan) {
            return schema()->instance('Slogan');
        }

        return null;
    }
}

Not sure if I need to define a real model, but if I want a relay paginater, the @paginate directive in the query section seems to fail as soon as I don't..

Hope someone can shed some light on this.

you need a query resolver or, yes, a model.

Hi @kikoseijo,

thanks for the quick reply, but I can't seem to get a resolver to work with the @paginate directive, since it requires a Model. Since the SearchResult (or Submission, just a name) is not an actual model, it seems like I'm in a chicken and egg situation here: no relay pagination or a non existing model..

I'm sure there is a very simple way but aparently there's still another bug in Lighthouse since the resolveType function never even gets called.. I even tried die() in the Lighthouse InterfaceDirective's resolveType, no difference..

Hope to get this over with soon!

Interface with a resolver? hummm... Can you do that? no idea,..

You can build pagination on your own like this:

this is just and old code copied from the initial v2 version

public function resolve()
 {
        $data = Car::orderBy('id', 'DESC')->relayConnection($this->args);
        $pageInfo = (new ConnectionField)->pageInfoResolver($data,$this->args,$this->context,$this->info);

        $page = $data->currentPage();
        $edges = $data->values()->map(function ($item, $x) use ($page) {
            $cursor = ($x + 1) * $page;
            $encodedCursor = $this->encodeGlobalId('Car', $cursor);
            $globalId = $this->encodeGlobalId('Car', $item->getKey());
            $item->_id = $globalId;
            return ['cursor' => $encodedCursor, 'node' => $item];
        });


        return [
            'pageInfo' => $pageInfo,
            'edges' =>  $edges,
        ];
    }

Hi @kikoseijo,

thanks for providing this! Just to make things clear: where should I add this resolve function in a relay context? I'm sure you know by now that the @paginate directive needs a model attribute and doesn't work with a resolve function. I'll experiment a bit, but this kind of trial and error approach is very cumbersome to say the least.

As far as I understand, the @interface directive needs a resolver to resolve the type, but since there seems another bug with the relay implementation causing the Type resolver being ignored, I still have no luck. @chrissm79 Hopefully this is an easy fix as well, dying to get your take on this ;-)

Thanks again for all the great support!

In your query , just build a simple query and do what you want there....

Take this in consideration:
No Mather what you building the response to send on the resolver must be same type you define in your schema.

To make it simple: imagine you only working with arrays, build an array and send to your response.

Maybe this helps:

https://github.com/nuwave/lighthouse/issues/70

Was building a pagination by hand using original pagination directives @hasMany..

The trick was mention before was: (trying to explain better here)

Behind the scenes lighthouse can query records, but when you get to work with complex records best its build your own resolver.

Now, for the second part to work, thinking you not letting lighthouse build your query, in order to be able to provide the right data to be resolved, you bust build an array why any data you want, as many records you want, just, the structure of the data must be the way that having your schema and your query, grapqhl-php should be able to extract just the data you asking for in your query.

BTW, the pagination, the number of records, the search,,, must be done before resolving back, its up to you to provide the page number,,, etc...

This option require more work from your side, but helps understand it better.

Have fun!

Hi @kikoseijo,

once again I'm overlooking closed issues like the one you mentioned, despite me searching really hard on this!
With a Query class I was already able to get results, but the interfaced type would always return null, I guess the forementioned issue of the resolveType function never being called is to blame here..

Anyway, working with React Native has been proven quite challenging as well, so tomorrow I'll take a fresh start with this ;-)

Thanks again!

@chrissm79 Did you get a chance to have a look why the resolver defined in the @interface directive never seems to be called? Thanks in advance!
By the way: impressive work you've been doing in the @crud directive! (I was already wondering what was keeping you occupied ;-) )

Thanks again!

Erik

@4levels sorry, didn't realize there was an issue!

I have the following schema and everything seems to be working for me w/ the polymorphic relationship (the InterfaceResolver function is being called correctly):

interface Commentable @interface(resolver: "App\\GraphQL\\InterfaceResolver@commentable") {
  id: ID!
}

type Post implements Commentable {
  id: ID!
  title: String
  body: String
}

type Video implements Commentable {
  title: String
  url: String
}

type Comment {
  id: ID!
  body: String
  commentable: Commentable @belongsTo
}

type Query {
  comments: [Comment!]! @paginate(model: "App\\Comment", type: "relay")
}
<?php

namespace App\GraphQL;

use App\Video;

class InterfaceResolver
{
    public function commentable($value)
    {
        return $value instanceof Video
            ? schema()->instance('Video')
            : schema()->instance('Post');
    }
}

HI @chrissm79,

thanks for getting back so quickly! I'll be giving it another try anytime soon.
Am I correct to assume that you did add a polymorphic Eloquent relation?
So maybe a quick question: how would you go about this if you wanted a comments relation on a User model so I can paginate through it using relay?

Some more questions: from my understanding the relation would make sense if I'd be using the @union directive, because as far as I understood, the @interface directive would not declare any additional properties, hence there's no existing Eloquent relation between the records. As you can tell, I'm trying to avoid having to create a separate model for the sole purpose to have records of mixed types in one relay pageable relation (eg. User has many submissions, that are either Posts or Videos without having an actual Submission Eloquent model.

Thanks again!

Erik

Hey @4levels,

Yes, in my example project commentable represents a Polymorphic relationship. To get comments from a user, that's just like any other relationship (if you have a user_id foreign key on the comments table). So it would look like this:

type User {
  # ...
  comments: [Comment] @hasMany(type: "relay")
}

As for the Submission question, I think that's more of a query question rather than a GraphQL question. GraphQL can represent a mixed set of results with union or interface types. It does not require you do anything special w/ your DB (like creating a new model).

I think you're particular issue is that Laravel doesn't provide a way (at least not one that I know of) to define a single relationship with multiple models (that's not Polymorphic). To do this in GraphQL/Lighthouse, you'd have to create a custom resolver function that returns the result of your query similar to @kikoseijo example.

Alternatively, yes, you would have to create another Polymorphic model that holds the user_id and the submittable_id and submittable_type which points to the Video or Post since Laravel doesn't have another way of accomplishing this.

If I were building this from scratch and I preferred submissions be a single query/result I would probably set up my DB in the following way:

Schema::create('submissions', function (Blueprint $table) {
    $table->increments('id');
    $table->unsignedInteger('user_id');
    $table->string('title');
    $table->json('data');
    // Video: { url: "..." }
    // Post: { body: "..." }

    $table->foreign('user_id')->references('id')->on('users');
});
interface Submission @interface(resolver: "App\\GraphQL\\InterfaceResolver@submission") {
  title: String
}

type Post implements Submission {
  title: String
  body: String
}

type Video implements Submission {
  title: String
  url: String
}
namespace App\GraphQL;

class InterfaceResolver
{
    public function submission($value)
    {
        return isset($value->data['url'])
            ? schema()->instance('Video')
            : schema()->instance('Post');
    }
}

This way I could have a simple submissions relationship on my User model. If the Submission's data array has the url set then that means it's a Video otherwise it would be a Post. Hopefully that helps :-)

Hi @chrissm79,

thanks again for the elaborate explanation!
The reason I'm so stubbornly looking for a relation instead of a query is that from the relay perspective, they kind of like to have all graphql queries be related to the current user (viewer or me), which means every record should be somehow related to a user..
I guess I'll be saving myself much more headaches if I'd just let go of this design principle (despite me actually liking it) and since Lighthouse already provides an easy integration with the current authenticated user thanks to your @auth directive, this could really mean "plain sailing" from now on :smile:
And with the great examples of oa @kikoseijo I think I'll manage to even pull it off eventually, without having to stumble upon other hurdles down the road (like the Laravel Query Builder bug with union queries). I'll definitely report back here how I went about it finally.

As I'm currently releasing my first beta version of the real thing (as in a real .apk file) and I already tripped over the java version bug in android_sdk cli and the SSL bug in Android 7.0 (luckily deploying servers and managing Nginx is my cup of tea) so I'm expecting some more bumps as I'm getting into this further.

Thanks again for all your great support and helpful replies in this thread, couldn't have done it without you guys!

Hey @4levels, are we okay to close this one or are you still encountering some issues? Let me know if you still have any questions!

Hi @chrissm79,

I have been searching for days now to get at least something going with having a list of mixed results whilst using a relay paginator, I think I've been so close to the solution so many times, but I'm very sure I keep missing things everytime, preventing me form achieving this. Since I still didn't manage to achieve this (IMHO trivial) task (considering Lightouse's en Eloquent's power), I don't feel like closing this issue just yet..

Can you please help me out here? All I want is a relay paginatable list (relation or query, I don't care anymore), containing records of mixed types without having to make a new database model just to achieve this so I can finally have a list of mixed results showing up in my React Native app..

Thanks again!

Hey @4levels, I can help out with creating the right output but can you put your Eloquent query here so I can show you out to generate the right resolver? Thanks!

Hi @chrissm79,

this is what I currently have in schema.graphql


interface Searchable @interface(
    resolver: "App\\GraphQL\\Interfaces\\Searchable@resolveType"
  ) {
  id: ID! @globalId
}
type SearchResult {
  id: ID! @globalId
  # resolves to either a Image or Slogan type
  searchable: Searchable
}
type Image implements Searchable @model {
  id: ID!
  filename: String!
}
type Slogan implements Searchable @model {
  id: ID!
  slogan: String!
}
type Query {
  viewer: User @auth
  submissions: [SearchResult] @paginate(type: "relay", model: "SearchResult")
}

I created a model class (that is no real model, just a class) like so in app/Models/SearchResult.php

namespace App\Models;
use Nuwave\Lighthouse\Support\Traits\IsRelayConnection;
class SearchResult {
    use IsRelayConnection;
    public static function query() {
        $images = Image::query()
            ->select(['id', 'filename as data'])
        ;
        $slogans = Slogan::query()
            ->select(['id', 'slogan as data'])
            ->union($images)
        ;
        return $slogans->limit(5);
    }
}

I did patch the laravel union query pagination bug - laravel/framework/issues/14837

My test query runs and even returns the correct number of results, just the searchable attribute remains null

{
  submissions (first: 5) {
    edges {
      node {
        id
        searchable {
          ...SRIFields
          ...SRSFields
          ...SRAFields
        }    
      }
    }
  }
}
fragment SRIFields on Image {
  id
  filename
}
fragment SRSFields on Slogan {
  id
  slogan
}
fragment SRAFields on Artwork {
  id
  rating
}

with results:

{
  "data": {
    "submissions": {
      "edges": [
        {
          "node": {
            "id": "U2VhcmNoUmVzdWx0OjI4OA==",
            "searchable": null
          }
        },
        {
          "node": {
            "id": "U2VhcmNoUmVzdWx0OjMzOQ==",
            "searchable": null
          }
        },
        // and so on

And the interface resolver is here
app/GraphQL/Interfaces/Searchable.php

namespace App\GraphQL\Interfaces;
use App\Models\Image;
use App\Models\Slogan;
class Searchable
{
    public function resolveType($value)
    {
        error_log("\n" . get_class($value), 3, '/tmp/debug.txt');
        if ($value instanceof Image) {
            return schema()->instance('Image');
        } else if ($value instanceof Slogan) {
            return schema()->instance('Slogan');
        }
    }
}

Please note that this class never gets called at all as mentioned before: no output in /tmp/debug.txt, no matter what I write in there..

Hi @chrissm79,

I tried refactoring everyhting to use the @union directive, but I end up in a very similar situation. The union resolver is being called, but the results are still null

This was all trying to get a @paginate directive with the type "relay" working.

If I use a non-paginator approach, I'm not undestanding how to instruct lighthouse that my query is returning a relay type paginator using mixed models. I saw the uses of the @pagination directive but that one doesn't exist. In short, I'm just not getting how to tell ligthhouse to create a relay paginator from a resultset (since I can union different models with Eloquent), which seems very trivial to achieve (hence my hard time)

Thanks again for sticking with me!

Hi @chrissm79, it seems that when using the union resolver, Eloquent thinks all records are of the last type in the union query, trashing the resolver since it always recieves the same model class. Not yet sure how to try to go about this...

FYI, I don't expect a simple copy paste solution, just an example of how you would achieve a similar thing:
having a relay paginator containing different types, without having to declare another database model with relations (as with the polymorphic relations)..

Thanks for the info @4levels, just so I can test things out can you do me a favor and paste your models and DB schema files here? Just the basic information that relates everything together would be fine, but that will help me re-create your environment to test with (or if you have a repo I can look at that would work nicely too 馃槃).

If you run the following:

public static function query() {
    $images = Image::query()
        ->select(['id', 'filename as data'])
    ;
    $slogans = Slogan::query()
        ->select(['id', 'slogan as data'])
        ->union($images)
    ;
    return $slogans->limit(5);
}

does it give you a mixed list of Image and Slogan models or just it just give you a list of Image models, some w/ the Slogan columns? I ran a similar test awhile ago and all the results came back as the first model in the query.

HI @chrissm79, that's exactly what's happening here too: all results are returned and hydrated as Slogan records.
I'll put a repo online so you can hopefully see what's going on.

When using the @interface resolver it bugs me that the resolver never seems to be called, as this seems the exact issue! Do you have a working example somewhere using the @interface directive?

Thanks again!

Hi @chrissm79,

I just created a new repo here on Github - https://github.com/4levels/api-test
I did leave out quite some info, please ask if you need more files..

Hope this helps!

I'll adjust your repo (if needed) to show how to get the @interface directive working but I assume something is going on at the query level. You almost certainly won't be able to use the built in paginate directive, but that's not too hard to get around since this is at the root query level... a custom field needs to be created along w/ some additional types.

@4levels thanks! I'll dig into this afternoon and can hopefully provide a solution!

It's quite a mess sometimes due to my numerous trial and effort attempts .. Thanks again!

HI @chrissm79, that's exactly what's happening here too: all results are returned and hydrated as Slogan records.

Okay, so this is the first issue because even if you were able to get Lighthouse to query the data correctly, you still aren't getting the correct models hydrated so things down the line (i.e., relationships, mutators, etc wouldn't work because it's been hydrated into the wrong model/class). This isn't a Lighthouse issue but rather a data issue and I'd really suggest you restructure your data as mentioned here.

To get around this, you could map over your results, check if a column that only belongs to a certain model is present and if so, convert it to that model. I'll see if I can put something to go off of that you can reference but give me some time 馃槃

EDIT
You are unable to use a union query to ask for different columns on different tables. Further explained in the next comment.

@4levels Okay, the deeper I dig into this the clearer it is that the data needs to be restructured in order for this to work well. But again, this isn't a Lighthouse/GraphQL issue, this is a data structure issue. You're root problem is that you're trying to union two different tables w/ different data which isn't supported in Eloquent (or any ORM that I'm aware of).

If you were to union two different tables in SQL, you need to query for the same exact columns (totally unrelated to Eloquent). If that's the case, then you don't need a Relay connection with different types because they now they have the exact same fields from your query. You could run such a query and create a new GraphQL type with the combined fields, but I personally wouldn't go that route.

For example, let's say you have the following data types:

type Post {
  title: String!
  body: String!
}

type Video {
  title: String!
  url: String!
}

Since we have to query the same columns when doing a union in SQL, your query would look something like the following:

(SELECT title, body as data FROM posts) UNION (SELECT title, url as data FROM videos);

Then we could do the following in GraphQL:

type Submittable {
  title: String;
  data: String;
}

However, this isn't the route I would go since you lose all your relationships and any other data that you might not be able to fit into a union properly.

If you don't need pagination, then you could just do the following:

type Video implements Submittable {
  # ...
}

type Post implements Submittable {
  # ...
}

type Query {
  submittable: [Submittable] @field(resolver: "MyResolver@submittable")
}

Then you're resolver would look something like this:

class MyResolver
{
    public function submittable()
    {
        $videos = App\Video::all();
        $posts = App\Post::all();

        return $videos->merge($posts->all());
    }
}

The only other two ways I can picture this working is here, and requires a new model/table or a single table for both models w/ a JSON column that holds the data that's unique for each type. But that would be the same regardless if you were working w/ GraphQL or event REST as you are attempting to paginate two different models w/ a single query. Either way, Lighthouse can handle both of those scenarios as I utilize both approaches in my project.

If you are able to create a single query w/ multiple models & unique columns (paginated) paste it here and we'll revisit this, but for now I'd strongly encourage the DB structure changes.

Hi @chrissm79,

thank you so much for your in depth explanation. Coming from a symfony/doctrine world, I guess these where the bits and pieces I couldn't figure out myself.
I'm quite familiar with hydration issues as Doctrine had quite some (performance) issues as well and I often had to fall back to using raw queries. Since I only started working with Lumen last year, I'm now wondering how the standard approach for Eloquent's union would hydrate results (obviously without pagination - could be the reason why the 2 year old Eloquent query builder bug still exists). I'm going to give this a run tomorrowm as it's well over 2am over here.

I might consider refactoring the database, but as you've already guessed, there's a lot more going on with these models than one can see from my code snippets here and I'm pretty sure that I'll have to stick with the current separation, both in database and model land.

Did you find an explanation why the resolver in the @interface directive never seems to be reached? Regardless of what records I return in a custom query, I'd expect at least the resolver would be hit once per found result. I'm still not 100% clear on how to correctly implement the @interface directive (especially the Eloquent bits) as it seems like it's doing very similar things as @union. Most of the info I found seems to suggest that they're almost interchangeable. I've already seen the exact same models / schema using both directives, hence my suspicion there might be still something off with the @interface directive similar to the bug with the @union directive. Commentable as a polymorphic relation with @union totally makes sence, but Searchable doesn't seem to fit for this as I just can't imagine myself creating a dedicated table for search results with all the extra logic involved to keep it in sync.

Since I'm considering myself still a beginner when it comes to Eloquent, I'm still wondering how it's inheritance is supposed to work then. In Doctrine there were different ways of having polymorphic relationships with the materialized version being my favorite, since it would use separate tables with different columns for each type, yet allow inheritance of model relations, events and methods. Eg. a document model with a workflow logic alongside an invoice model, in a separate table, extending the document model, has proven to be very handy to say the least.

This still leaves me puzzled though about a very similar use case: search
Since search obviously doesn't require to store results as records in a database, I'll start with checking the various links you've posted so far to see how I can handle this gracefully. The end goal is still to use relay to page through a list of results, being of very different types. I don't want to add hydration logic on the client side so I'm definitely looking into getting GraphQL (with it's hydration aka fragment features) to do the heavy lifting here.
I'm familiar with Elasticsearch and keeping relational databases in sync with a more flat (and incredibly fast) external structure, but that will take me in a whole different direction (probably towards Laravel Scout). You can imagine I'm not feeling confident enough to implement this since Laravel has aparently still quite some secrets for me, left alone creating an Algolia replacement for Scout using Elastic. Personally I never even got why they chose Algolia over Elastic (we pbbly wouldn't be having this lenghty conversation if Elastic was the default driver), since it's a non-free service and Elastic is a no-brainer with it's huge knowledge base and proven stability.

Anyway, thanks again for your willingness to investigate my particular problem and shed some light on Laravel / Eloquent. If you already have references or posts on how you'd go about implementing search, with relay, please feel free let me know (or add them to the documentation.

Kind regards,

Erik

Hi @chrissm79,

after Googling a bit more, it seems like there really is no easy or efficient way to paginate through multiple models in Laravel / Eloquent and even the Scout package only allows for returning results of one type. So it seems Scout is handy to do the indexing with eg. Elastic (found some good looking driver projects), but when it comes to actually returning mixed records, no joy.
Everyone seems to be dumping different collections into arrays (bummer), merging them (bigger bummer) and creating a new paginator from the result (even bigger bummer as it cannot use the database anymore).. this seems a terrible approach as every single record will be hydrated twice: once to dump it to an array and once to build the new paginator! This will definitely start failing once you're dealing with +10k records. I can't even begin to imagine how one would be able to paginate correctly considered sorting using this approach. So my initial thought that this would be an easy task seems very wrong to say the least.

Is there really no simple way to use a union query (with limit and orderby) that just returns id's and type names, and then have a paginator that can resolve the actual model for each result when needed? Like I said before: this seems a very simple process that seems easy to acomplish using the powers of both Lighthouse and Eloquent.

Still regarding the resolver of the @interface directive: am I correct to assume that regardless of the Eloquent model relation behind the scenes, this method should at least be called once per returned result? I'm getting no errors and the resolver is simply ignored. To me this still sounds like a bug and I just can't handle searching for a needle in a haystack like I tried before with the @union directive and you managed to fix this in a jiffy. If the resolve method _would_ have been called, I'm sure I would have managed already using the forementioned union query approach and resolve each result to it's correct graphql type with Lighthouse. I'll be looking again at @kikoseijo 's approach as well.

So if you or anyone can just put a full @interface example, including the required steps in Eloquent, in the documentation, this issue can die in peace :smile:

Or wait, I'll create a new issue regarding the @interface resolve method not being called using Lumen / Relay and reference this issue there for further reading.
Feel free to close this issue if you've come this far. Thanks again!

Hi @chrissm79,

I think I'm getting very close! I'm getting mixed results from the resolve function in my query, it's just GraphQL not detecting the correct type, despite me returning the correct type in the Interface resolver..
So at least the InterfaceResolver is being called once now for each result, but somehow, because the paginator returns only Image records (known limitation of Eloquent), GraphQL somehow ignores this along the way and considers everything Images, despite me returning the correct type in the resolver.. maybe I'm still overlooking something or GraphQL / Lighthouse still uses the class from the paginator results instead of the resolved type class?

app/GraphQL/Queries/Search.php

namespace App\GraphQL\Queries;

use App\Models\Image;
use App\Models\Slogan;
use Nuwave\Lighthouse\Support\Schema\GraphQLQuery;
use Nuwave\Lighthouse\Support\Traits\HandlesGlobalId;

class Search extends GraphQLQuery
{
    use HandlesGlobalId;

    public function resolve()
    {
        $images = Image::query()->select('id', 'filename as data', 'created_at')
            ->selectRaw('"image" as type')
        ;
        $slogans = Slogan::query()->select('id', 'slogan as data', 'created_at')
            ->selectRaw('"slogan" as type')
            ->union($images)
        ;
        return $slogans->orderBy('created_at', 'DESC')->relayConnection($this->args);
    }
}

app/GraphQL/SearchResultInterface.php

namespace App\GraphQL;

class SearchResultInterface
{
    public function resolveType($value)
    {
        // use type from rawsql type value as Eloquent still casts all results as Images
        // $value->type contains the actual type from the union query, eg "image" or "slogan"
        // if error_logged to file the results are exactly what I need: images and slogans mixed, ordered by date
        return schema()->instance(ucfirst($value->type));
    }
}

app/GraphqQL/schema.graphql

interface SearchResult @interface(resolver: "App\\GraphQL\\SearchResultInterface@resolveType") {
  id: ID! @globalId
  created_at: DateTime
  data: String!
  type: String!
}
type Image implements SearchResult @model {
  id: ID! @globalId
  data: String! @rename(attribute: "filename")
  type: String!
  filename: String!
}
type Slogan implements SearchResult @model {
  id: ID! @globalId
  data: String! @rename(attribute: "slogan")
  type: String!
  slogan: String!
}
type Query {
  viewer: User @auth
  search (first: Int!, after: String): [SearchResult!]!
}

the query:

{
  search (first: 100) {
    id
    created_at
    type
    ...IFs
    ...SFs
  }
}
fragment IFs on Image {
  id
  filename
}
fragment SFs on Slogan {
  id
  slogan
}

and results, which are somehow still casted as Images despite some of them being slogans
graphql { "data": { "search": [ { "id": "QXJ0d29yazo2Nzc=", "created_at": "2018-05-27T14:00:05+02:00", "type": "image", "rating": null }, { "id": "QXJ0d29yazo2NzY=", "created_at": "2018-05-23T10:00:05+02:00", "type": "image", "rating": null }, ... }

Strangly I do get the following GraphQL error if I define type: String! (required) in the Interace:

Error: SearchResult.type expects type "String!" but Image.type provides type "String".

The query actually loads, but the introspection failes. Removing the requirement from the SearchResult declaration in the schema solves this however..

I'll be still experimenting around, as this error doesn't seem to happen with regular interface fields..

IT WORKS!!!

I just had to use a different column than type (I should have known since with all this magic happening underneath, type would pbbly clash with things..)
Still testing around as this is litteraly 20 seconds old :smile:

Too bad my solution still depends on the patch for the Eloquent query builder though..
I'll try to create another PR for it, but since my expectations with creating PR's for Laravel that will be accepted are pretty low, I might just resort to using a fork instead..

What a threat....

@4levels I beg it works, using type as a field name,,,, hahahahaha!
You probably got it wrong from the base, as per architecture or data structure.

Why you need to call a morphed type by its own? they only should exist as a relationship to the models they belong to.

Come on! you can make it!

@4levels Looks like there's been some good progress lately, hopefully you're close to the finish line!!!

we pbbly wouldn't be having this lenghty conversation if Elastic was the default driver

This is so very true! I've yet to use ElasticSearch in a project but I see that it can provide a mixed set of results that you could probably hydrate back into the proper Laravel models and get your collection of mixed types. Depending on how that's done you still might need to go a special route to get it into a Relay connection but it shouldn't be too difficult. That would make for an amazing community plugin btw!!

Too bad my solution still depends on the patch for the Eloquent query builder though

Maybe you could create your own Query builder, pass in the two builders that need to be unioned and handle things as needed in there so you don't have to worry about changing the source? Of course that could much easier said than done and I haven't event looked at what's available on the builder that you need, but something along the line of:

// Inside resolver...

$imageBuilder = Image::query()->select('id', 'filename as data', 'created_at')
    ->selectRaw('"image" as type');
$sloganBuilder = Slogan::query()->select('id', 'slogan as data', 'created_at')
    ->selectRaw('"slogan" as type');

return UnionQueryBuilder::union($imageBuilder, $sloganBuilder)
    ->orderBy('created_at', 'DESC')
    ->relayConnection($this->args);

// inside UnionQueryBuilder...

class UnionQueryBuilder
{
    public static function union($left, $right)
    {
        // based on your changes
        // https://github.com/laravel/framework/issues/14837#issuecomment-391667831

        $left->union($right);

        return $left->newQuery()
            ->selectRaw("COUNT(*) as aggregate FROM (".$left->toSql().") as aggregate_query")
            ->mergeBindings($left);
    }
}

Hi @chrissm79 and @kikoseijo,

I finally made it happen, but I had to use some old school tricks to trick Eloquent into believing me :smile:
In a nutshell, I created a SearchResult eloquent model with table that will never contain any data, with the columns / attributes id, searchable_id, searchable_type, data and created at.
I then used the forementioned trick to union the query builders and adding the expected searchable_* field data for each model. I could then use the morphTo on the SearchResult model and add plain belongsTo relations to the Image and Slogan models under the name searchable.
This results in me getting a basic search result where I can add arbitrary data using sql in the data column and have a searchable relation with the actual full record, all query-able and traversable by Relay! No time to paste code as I really need to hurry on with a deadline by tomorrow 4pm and I still have so much to do in RN!
Expect a full example as a gist (or in the docs issue) anytime soon.

Thanks for bearing with me guys! Your support means a lot since there's virtually no one else to share this with..

Was this page helpful?
0 / 5 - 0 ratings

Related issues

spawnia picture spawnia  路  3Comments

wimski picture wimski  路  3Comments

vine1993 picture vine1993  路  3Comments

basudebpalfreelancer picture basudebpalfreelancer  路  4Comments

spawnia picture spawnia  路  4Comments