timacdonald / json-api

A lightweight API resource for Laravel that helps you adhere to the JSON:API standard. Supports sparse fieldsets, compound documents, and more.
586 stars 40 forks source link

[Question] How to include tree relationships? #23

Closed juliomotol closed 1 year ago

juliomotol commented 1 year ago

We have a Menu and Node models where a Menu has many Nodes and a Node also has many child Nodes.

class Menu extends Model
{
    /** @return HasMany<Node> */
    public function nodeTrees(): HasMany
    {
        return $this->nodes()
            ->whereNull('parent_id') // ensure only root nodes will be fetched
            ->with('children');
    }
}

// and 

class Node extends Model implements Sortable
{
    /** @return HasMany<Node> */
    public function children(): HasMany
    {
        return $this->hasMany(self::class, 'parent_id')
            ->ordered() // scope from `spatie/eloquent-sortable`
            ->with('children');
    }
}

Then our controller action:

public function show($menu): MenuResource
{
    return MenuResource::make(
        QueryBuilder::for(Menu::find($menu))
            ->allowedIncludes(['nodeTrees'])
            ->firstOrFail()
    );
}

Now, when we try to /menus/9999?include=nodeTree, it only responds with the root nodes and doesn't show the child nodes nested in them. When try to inspect the model before MenuResource::make(), we do see whole node tree.

Any ideas on how this could be done?

timacdonald commented 1 year ago

Can you please show me the MenuResource implementation?

juliomotol commented 1 year ago

Here you go

class MenuResource extends JsonApiResource
{
    public function toAttributes($request): array
    {
        return  [
            'name' => $this->name,
        ];
    }

    public function toRelationships($request): array
    {
        return [
            'nodeTrees' => fn () => NodeResource::collection($this->nodeTrees),
        ];
    }
}

// and 

class NodeResource extends JsonApiResource
{
    public function toAttributes($request): array
    {
        return  [
            'label' => $this->label,
            'url' => $this->url,
            'target' => $this->target,
        ];
    }

    public function toRelationships($request): array
    {
        return [
            'children' => fn () => NodeResource::collection($this->children),
        ];
    }
}
timacdonald commented 1 year ago

Does the following result in the expected relationships,..

/menus/9999?include=nodeTree.children
juliomotol commented 1 year ago

Tried that but it only responds with up to the second level of the tree:

array:6 [ // app/HttpTenantApi/Controllers/Menu/MenuController.php:30
  "id" => 1
  "name" => "Menu"
  "node_trees" => array:1 [
    0 => array:10 [
      "id" => 1
      "label" => "Node 1 "
      "menu_id" => 1
      "parent_id" => null
      "order" => 1
      "children" => array:1 [ // included in the response
        0 => array:10 [
          "id" => 2
          "label" => "Nested Node 1"
          "menu_id" => 1
          "parent_id" => 1
          "order" => 1
          "children" => array:1 [ // not returned in the response
            0 => array:10 [
              "id" => 3
              "label" => "Deeply Nested Node"
              "menu_id" => 1
              "parent_id" => 2
              "order" => 1
              "children" => []
            ]
          ]
        ]
      ]
    ]
  ]
]
timacdonald commented 1 year ago

So the issue here is that your model relationships are not setup to return all of the tree.

I recommend using this package to add a relationship to your model that may be included and will return all of the expected results: https://github.com/staudenmeir/laravel-adjacency-list

juliomotol commented 1 year ago

I initially tried that, but gave up halfway. I'll try to fiddle with it more and get back to you. Thanks!

timacdonald commented 1 year ago

Sounds good.