While asking one of my "How do I..." questions - specifically this one I realised this likely warrants some external discussion.
Right now I'm thinking that the ideal way to do this if we have some additional meta data is going to be manually setting up a link model e.g. UserCompanyLink and then rather than using belongsToMany use hasMany/belongsTo relations so a query would then look like
{
users {
data {
id
name
companies {
id #this is the relationship id
role #this is one of the "pivot" fields
company {
id #this is the company id
name
}
}
}
}
}
For mutations this gives you a lot of options, You could either do a mutation with the input like
{
"id": 5,
"name": "Donald Trump",
"companies": [
{"id": 8, "role": "owner"},
{"id": 12, "role": "employee"}
]
}
And then assume that if the companies key is present it always represents the full list of companies the user is linked to.
The issue with this method is that a) it requires a custom resolver, b) you need to always pass across the full list of companies. The good thing is you can manage the relationships in bulk.
Alternatively you could treat the UserCompanyLink as a full entity and add createUserCompanyLink, deleteUserCompanyLink, and updateUserCompanyLink mutations.
This second method is likely what I'll go for, although I feel there might be a better way than skipping the belongsToMany relations in laravel and also it becomes a bit of a message in relay as your query looks like
{
users {
edges {
cursor
node {
id
name
companies {
edges {
cursor
node {
id #this is the relationship id
role #this is one of the "pivot" fields
company {
id #this is the company id
name
}
}
}
}
}
}
}
}
When really the ideal output in my mind would be this
(note I haven't actually thought this through from the caching side of frontend clients)
{
users {
edges {
cursor
node {
id
name
companies {
edges {
cursor
role #this is one of the "pivot" fields
id #this is the relationship id
node {
id #this is the company id
name
}
}
}
}
}
}
}
What's our thoughts around this?
@hailwood did you see this?
https://github.com/chrissm79/lighthouse-belongs-to-many
I found very useful the getFieldAttribute on models to do most of the hard work on relationships.
Hey @kikoseijo yep that works great for belongsToMany, unfortunately it doesn't help with the case where the relationship has extra data attached to it (e,g, if you want the date the car option was added)
@hailwood what about this?
type Task {
title: String!
pivot: TaskPivot
}
type TaskPivot {
user_id: Int!
task_id: Int!
pivot_field: String
}
This works, here its a project for you to test it, last commits have the adjustments.
https://github.com/kikoseijo/lighthouse-example
Here its the query used:
https://github.com/kikoseijo/lighthouse-example/blob/master/GraphQL-references.md
@kikoseijo I had originally thought about nesting them under the model, and I'd be happy to be wrong on this, but let's say we're displaying a page that lists all uncompleted tasks per user.
If user A is linked to task A with the pivot_field being foo and user B is linked to task A with the pivot_field being bar then when we again grab task A for user B's list, won't that also make it appear in user A's list that pivot_field is bar due to caching by ID in the frontend clients (relay/apollo)?
Now I get you point @hailwood the cache error I see it happening if both users use same device. Relay cache its done inside devices, but you can use different stores and avoid caching problems, cache its not configured by default, needs be programmed, as far as I now in Relay.
I can clearly see your point and the problem when a server side cache its applied.
Pivot information will be always linked to the current user id, a way to go around could be (not sure) to use an unique root query, being this for example the user id, with a relationship to the Tasks. You cant just call give me task X, you must do something like give me user X task X.
I haven't worked server cache and cant go any further.
BTW: thanks for this excellent mental exercise, maybe the Boss can give us his thoughts on this.
@hailwood So my initial thoughts are pretty close to what @kikoseijo mentioned. Technically, you could do the majority of this the way it's was listed in the example, but we could benefit from a @pivot directive here as well.
So, w/out creating your own directive, you could do the following
type Job {
tasks: [TaskPivot] @hasMany
}
type Task {
title: String!
}
type TaskPivot implements Node
@node(
resolver: "App\\Http\\GraphQL\\TaskPivot\\\NodeTest@resolveNode"
typeResolver: "App\\Http\\GraphQL\\TaskPivot\\\NodeTest@resolveType"
) {
_id: ID! @field(resolver: "App\\Http\\GraphQL\\TaskPivot@id")
user_id: Int! @field(resolver: "App\\Http\\GraphQL\\TaskPivot@userId")
task_id: Int! @field(resolver: "App\\Http\\GraphQL\\TaskPivot@taskId")
completed: String @field(resolver: "App\\Http\\GraphQL\\TaskPivot@completed")
task: Task @field(resolver: "App\\Http\\GraphQL\\TaskPivot@task")
}
Then your resolver class would look something like this:
use Nuwave\Lighthouse\Support\Traits\HandlesGlobalId;
class TaskPivotType
{
use HandlesGlobalId;
public function id($root)
{
// For a composite key you could do something like this:
return $this->encodeGlobalId(
'TaskPivot',
"{$root->pivot->user_id}-{$task->pivot->task_id}"
);
}
public function completed($root)
{
return $root->pivot->completed;
}
public function task($root)
{
return $root;
}
public function resolveNode($id)
{
// This would be the custom composite key we created above
list($userId, $taskId) = explode('-', $id);
return \App\Task::find($taskId);
}
public function resolveType($value)
{
if ($value instanceof \App\Task) {
return schema()->type('Task');
}
}
}
To be completely honest, I'm going off the top of my head so I haven't tested this but I believe this solves all your problems. I'll add a pivot directive that allows you to do the following which will save you from having to create a custom resolver for each field.:
type TaskPivot implements Node
@pivot(modelField: "task")
@node(
resolver: "App\\Http\\GraphQL\\TaskPivot\\\NodeTest@resolveNode"
typeResolver: "App\\Http\\GraphQL\\TaskPivot\\\NodeTest@resolveType"
) {
_id: ID! @field(resolver: "App\\Http\\GraphQL\\TaskPivot@id")
user_id: Int!
task_id: Int!
completed: String
task: Task
}
Let me know if you still see any issues here. Thanks @hailwood and @kikoseijo!!
@chrissm79 Did you ever take a stab at the @pivot directive? Seems like that would be incredibly useful! I'll definitely be needing to make strong use of pivot data in my current project so that would be awesome. As I improve I may try to take a stab at it myself, but I'm still wrapping my head around all of this.