Closed 4levels closed 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:
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": "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"
}
}
]
}
}
}
}
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:
```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
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.
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