nuwave / lighthouse

A framework for serving GraphQL from Laravel
https://lighthouse-php.com
MIT License
3.37k stars 438 forks source link

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

Closed 4levels closed 6 years ago

4levels commented 6 years ago

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

4levels commented 6 years ago

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:

4levels commented 6 years ago

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..

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": "admin@test.com",
      "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"
            }
          }
        ]
      }
    }
  }
}
kikoseijo commented 6 years ago

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

Try againg with a recent commit!

4levels commented 6 years ago

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

chrissm79 commented 6 years ago

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!

4levels commented 6 years ago

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

4levels commented 6 years ago

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:

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:
```graphql
{
  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
4levels commented 6 years ago

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.

4levels commented 6 years ago

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);
    }
chrissm79 commented 6 years ago

@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.

4levels commented 6 years ago

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)

chrissm79 commented 6 years ago

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

4levels commented 6 years ago

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

4levels commented 6 years ago

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

4levels commented 6 years ago

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

4levels commented 6 years ago

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..

chrissm79 commented 6 years ago

@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 😄

4levels commented 6 years ago

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

4levels commented 6 years ago

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..

4levels commented 6 years ago

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!

4levels commented 6 years ago

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..

4levels commented 6 years ago

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.

kikoseijo commented 6 years ago

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

4levels commented 6 years ago

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!

kikoseijo commented 6 years ago

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,
        ];
    }
4levels commented 6 years ago

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!

kikoseijo commented 6 years ago

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.

kikoseijo commented 6 years ago

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!

4levels commented 6 years ago

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!

4levels commented 6 years ago

@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

chrissm79 commented 6 years ago

@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');
    }
}
4levels commented 6 years ago

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

chrissm79 commented 6 years ago

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

4levels commented 6 years ago

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!

chrissm79 commented 6 years ago

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!

4levels commented 6 years ago

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!

chrissm79 commented 6 years ago

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!

4levels commented 6 years ago

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

4levels commented 6 years ago

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
4levels commented 6 years ago

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..

4levels commented 6 years ago

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!

4levels commented 6 years ago

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...

4levels commented 6 years ago

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

chrissm79 commented 6 years ago

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.

4levels commented 6 years ago

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!

4levels commented 6 years ago

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!

chrissm79 commented 6 years ago

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.

chrissm79 commented 6 years ago

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

4levels commented 6 years ago

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

chrissm79 commented 6 years ago

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.