FriendsOfCake / crud-json-api

Build advanced JSON API Servers with almost no code.
https://crud-json-api.readthedocs.io/
MIT License
56 stars 32 forks source link

Does not meet the 1.0 spec for fetching relationships #116

Closed geoidesic closed 4 years ago

geoidesic commented 4 years ago

According to the Spec: https://jsonapi.org/format/#fetching-relationships One should be able to fetch just the related data for a given record using URL of the following format:

http://{{domain}}/api/restaurants/1/dishes

This doesn't not work with crud-json-api, it gives an error:

2020-07-21 07:42:20 Error: [Cake\Controller\Exception\MissingActionException] Action RestaurantsController::1() could not be found, or is not accessible. in /Users/me/vendor/cakephp/cakephp/src/Controller/Controller.php on line 503
Stack Trace:
- /Users/me/vendor/friendsofcake/crud/src/Controller/ControllerTrait.php:43 

My routes look like this:

Router::prefix('api', function (RouteBuilder $routes) {
  $routes->resources('Restaurants');
  $routes->resources('Dishes');
}
geoidesic commented 4 years ago

My suggested solution is that a Trait be added to support this feature. The reason I suggest this is that crud-json-api is a bit of a strange beast – it's intended to support the JSONAPI spec but as it's an extension of crud it only really extends that libraries existing features so as to make them JSONAPI compliant. However there are parts of the JSONAPI spec that fall outside of the remit of crud. This is one such example.

One way around this is to add a Trait that can be used optionally to add this functionality. E.g.

trait CrudJsonApiControllerTrait
{
    /**
     * related method
     *
     * @return \Cake\Http\Response|null
     */
    public function related()
    {
        $pass = $this->request->getParam('pass');
        $id = $pass[0];
        $relation = $pass[1];
        $this->Crud->on('beforePaginate', function (\Cake\Event\Event $event) use ($id, $relation) {
            $event->getSubject()->query
                ->innerJoinWith(ucfirst($relation))
                ->where([ucfirst($relation) . '.id' => (int) $id]);
        });

        return $this->Crud->execute('index');
    }
}

Along with which you will need the route...

    $routes->connect('/{relation}/{id}/dishes', ['controller' => 'Dishes', 'action' => 'related'], ['id' => '\d+', 'pass' => ['id', 'relation']]);

Now you can make a crud controller support this part of the JSONAPI spec by simply setting the corresponding route and using this Trait.

dakota commented 4 years ago

This is already supported, but perhaps not documented.

You need to create a nested resource route as per https://book.cakephp.org/4/en/development/routing.html#creating-nested-resource-routes

You can then use $this->request->getParam('article_id') within the controller. I recommend using the hinted at prefix routing for the relationship controllers.

dakota commented 4 years ago

I would also recommend using a crud action instead of a trait (In keeping with the action object pattern used in Crud)

geoidesic commented 4 years ago

Good to know about the nested resource route option, however It still requires custom controllers / actions. It would be better I think if this plugin handled that internally. I'm just not sure how to implement that.

I.e. given such a URL, one should not have to custom build an Action, the plugin should recognise the URL format and apply the where clause automatically. Wouldn't you agree?

geoidesic commented 4 years ago

I would also recommend using a crud action instead of a trait (In keeping with the action object pattern used in Crud)

That makes sense.

The crud-json-api plugin doesn't have any custom actions, it relies solely on the standard CRUD actions supplied by crud. So maybe the way forward is to add custom Actions to the crud-json-api plugin that cater for these special cases.

These can then be mapped in the component config e.g.:

$this->loadComponent('Crud.Crud', [
            'actions' => [
                'Crud.Index',
                'Crud.View',
                'Crud.Add',
                'Crud.Edit',
                'Crud.Delete',
                'Crud.Bulk',
                'CrudJsonApi.RelationshipsIndex`,
                'CrudJsonApi.RelationshipsEdit`
            ],
geoidesic commented 4 years ago

I would also recommend using a crud action instead of a trait (In keeping with the action object pattern used in Crud)

I'm busy trying this but I don't seem to have access to the request data in a crud action class.

E.g. in my trait I had an action method like this:

public function updaterelationships()
    {
        $request = $this->request;
        $id = $request->getParam('id');
        $foreignTableName = $request->getParam('foreignTableName');
        $data = $request->getData();

And $data would then hold the request body i.e.:

Array
(
    [data] => Array
        (
            [0] => Array
                (
                    [type] => dishes
                    [id] => 3c9b642f-6682-4b7a-aff2-000000000043
                )
        )
)

However, once I refactor it as RelationshipsAction::_patch then I don't seem to have access to the request body any longer. E.g.

class RelationshipsAction extends BaseAction
{
     protected function _patch()
    {
          print_r($this->_request()->getData())

yields an empty array.

Any ideas? Obviously I need access to the request body to process it.

geoidesic commented 4 years ago

Fixed in current version of cake-4.x branch.