FriendsOfCake / crud-json-api

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

Relationships, links, self provides a link that results in an internal server error 500 #129

Closed geoidesic closed 4 years ago

geoidesic commented 4 years ago

With the following schemas:

$this->table('restaurants', ['id' => false, 'primary_key' => 'id'])
            ->addColumn('id', 'uuid')
            ->addColumn('name', 'string')
            ->addColumn('address', 'string')
            ->addColumn('created', 'datetime')
            ->addColumn('modified', 'datetime')
            ->create();

        $this->table('dishes', ['id' => false, 'primary_key' => 'id'])
            ->addColumn('id', 'uuid')
            ->addColumn('name', 'string')
            ->addColumn('rating', 'integer', ['signed' => false])
            ->addColumn('created', 'datetime')
            ->addColumn('modified', 'datetime')
            ->create();

        $this->table('dishes_restaurants')
            ->addColumn('restaurant_id', 'uuid')
            ->addColumn('dish_id', 'uuid')
            ->create();

And standard baking. Using routes like so:

$this->resources = [
    'Restaurants',
    'Dishes',
];

Router::prefix('api', function (RouteBuilder $routes) {

    JsonApiRoutes::mapModels($this->resources, $routes);
}

The route to get dishes related to a given restaurant would be:

http://{{domain}}/api/restaurants/3c9b642f-6682-4b7a-aff2-000000000038/dishes

This produces JSON:API output like so:

{
    "meta": {
        "record_count": 2,
        "page_count": 1,
        "page_limit": null
    },
    "links": {
        "self": "/api/dishes?page=1",                                         <--- PROBLEM 1
        "first": "/api/dishes?page=1",                                         <--- PROBLEM 1
        "last": "/api/dishes?page=1"                                           <--- PROBLEM 1
    },
    "data": [
        {
            "type": "dishes",
            "id": "3c9b642f-6682-4b7a-aff2-000000000040",
            "attributes": {
                "_matching_data": [],
                "created": "2020-09-05T16:29:43+00:00",
                "modified": "2020-09-05T16:29:43+00:00",
                "name": "Barbecue Burger",
                "rating": 5
            },
            "relationships": {
                "restaurants": {
                    "links": {
                        "self": "/api/dishes/relationships"               <--- PROBLEM 2
                    }
                },
                "dishes_restaurants": {
                    "links": {
                        "self": "/api/dishes/relationships"                <--- PROBLEM 2
                    }
                }
            },
            "links": {
                "self": "/api/dishes/view/3c9b642f-6682-4b7a-aff2-000000000040"
            }
        },
        {
            "type": "dishes",
            "id": "3c9b642f-6682-4b7a-aff2-000000000041",
            "attributes": {
                "_matching_data": [],
                "created": "2020-09-05T16:29:43+00:00",
                "modified": "2020-09-05T16:29:43+00:00",
                "name": "Slider",
                "rating": 3
            },
            "relationships": {
                "restaurants": {
                    "links": {
                        "self": "/api/dishes/relationships"                 <--- PROBLEM 2
                    }
                },
                "dishes_restaurants": {
                    "links": {
                        "self": "/api/dishes/relationships"                 <--- PROBLEM 2
                    }
                }
            },
            "links": {
                "self": "/api/dishes/view/3c9b642f-6682-4b7a-aff2-000000000041"
            }
        }
    ]
}

I've highlighted the problems with <--- PROBLEM in the output.

PROBLEM 1 – These links, if followed, display ALL dishes, not those associated to Restaurants. Is this expected? PROBLEM 2 – These links, if followed produce an error:

2020-09-05 15:52:20 Error: [TypeError] Argument 1 passed to Cake\ORM\Table::getAssociation() must be of the type string, null given, called in /Users/me/vendor/friendsofcake/crud-json-api/src/Action/RelationshipsAction.php on line 154 in /Users/me/vendor/cakephp/cakephp/src/ORM/Table.php on line 893
Stack Trace:
- /Users/me/vendor/friendsofcake/crud-json-api/src/Action/RelationshipsAction.php:154
- /Users/me/vendor/friendsofcake/crud-json-api/src/Action/RelationshipsAction.php:233
- /Users/me/vendor/friendsofcake/crud/src/Action/BaseAction.php:62
- /Users/me/vendor/friendsofcake/crud/src/Controller/Component/CrudComponent.php:244
- /Users/me/vendor/friendsofcake/crud/src/Controller/ControllerTrait.php:68
- /Users/me/vendor/cakephp/cakephp/src/Controller/ControllerFactory.php:81
- /Users/me/vendor/cakephp/cakephp/src/Http/BaseApplication.php:251
- /Users/me/vendor/cakephp/cakephp/src/Http/Runner.php:77
- /Users/me/vendor/cakephp/cakephp/src/Http/Runner.php:77
- /Users/me/vendor/cakephp/cakephp/src/Http/Middleware/BodyParserMiddleware.php:159
- /Users/me/vendor/cakephp/cakephp/src/Http/Runner.php:73
- /Users/me/plugins/Cors/src/Middleware/CorsMiddleware.php:39
- /Users/me/vendor/cakephp/cakephp/src/Http/Runner.php:73
- /Users/me/vendor/cakephp/cakephp/src/Http/Runner.php:58
- /Users/me/vendor/cakephp/cakephp/src/Routing/Middleware/RoutingMiddleware.php:172
- /Users/me/vendor/cakephp/cakephp/src/Http/Runner.php:73
- /Users/me/vendor/cakephp/cakephp/src/Routing/Middleware/AssetMiddleware.php:68
- /Users/me/vendor/cakephp/cakephp/src/Http/Runner.php:73
- /Users/me/vendor/cakephp/cakephp/src/Error/Middleware/ErrorHandlerMiddleware.php:121
- /Users/me/vendor/cakephp/cakephp/src/Http/Runner.php:73
- /Users/me/vendor/cakephp/cakephp/src/Http/Runner.php:58
- /Users/me/vendor/cakephp/cakephp/src/Http/Server.php:90
- /Users/me/webroot/index.php:40

Request URL: /api/dishes/relationships
Referer URL: http://localhost:8080/
Client IP: 127.0.0.1

I have tested this with normal id's (i.e. without using uuid in the schema and URLs) and I get the same issue.

The error is because there is no type placeholder part of the URL. My thoughts are that these URLs are malformed. What does the JSON:API spec say about them?

geoidesic commented 4 years ago

Also a question: why is it generating dishes_restaurants relationships, when this is already covered by dishes and restaurants?

dakota commented 4 years ago

Definitely something not right there. Could you paste the output of cake routes?

dakota commented 4 years ago

The issues seems to be either the incorrect routes are being generated, or the incorrect routes are being used.

geoidesic commented 4 years ago

Ok so firstly the associations...

On DishesTable

$this->belongsToMany('Restaurants', [
            'foreignKey' => 'dish_id',
            'targetForeignKey' => 'restaurant_id',
            'joinTable' => 'dishes_restaurants',
        ]);

On RestaurantsTable

$this->belongsToMany('Dishes', [
            'foreignKey' => 'restaurant_id',
            'targetForeignKey' => 'dish_id',
            'joinTable' => 'dishes_restaurants',
        ]);

The DishesRestaurantsTable generated by bake also has these associations:

$this->belongsTo('Restaurants', [
            'foreignKey' => 'restaurant_id',
            'joinType' => 'INNER',
        ]);
        $this->belongsTo('Dishes', [
            'foreignKey' => 'dish_id',
            'joinType' => 'INNER',
        ]);

Then the output from cake routes

+-------------------------------------+------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------+
| Route name                          | URI template                                         | Defaults                                                                                                                        |
+-------------------------------------+------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------+
| schema:list                         | /schema                                              | {"_method":"GET","action":"list","controller":"Schema","plugin":null}                                                           |
| schema:_controller:view             | /schema/{controller}                                 | {"_method":"GET","action":"view","plugin":null,"prefix":"Schema"}                                                               |
| ping                                | /api/ping                                            | {"_method":"GET","action":"index","controller":"Pings","plugin":null,"prefix":"Api"}                                            |
| api:pings:index                     | /api/ping                                            | {"_method":"OPTIONS","action":"index","controller":"Pings","plugin":null,"prefix":"Api"}                                        |
| api:restaurants:index               | /api/restaurants                                     | {"_method":"GET","action":"index","controller":"Restaurants","plugin":null,"prefix":"Api"}                                      |
| api:restaurants:add                 | /api/restaurants                                     | {"_method":"POST","action":"add","controller":"Restaurants","plugin":null,"prefix":"Api"}                                       |
| api:restaurants:view                | /api/restaurants/:id                                 | {"_method":"GET","action":"view","controller":"Restaurants","plugin":null,"prefix":"Api"}                                       |
| api:restaurants:edit                | /api/restaurants/:id                                 | {"_method":["PUT","PATCH"],"action":"edit","controller":"Restaurants","plugin":null,"prefix":"Api"}                             |
| api:restaurants:delete              | /api/restaurants/:id                                 | {"_method":"DELETE","action":"delete","controller":"Restaurants","plugin":null,"prefix":"Api"}                                  |
| api:restaurants:relationships       | /api/restaurants/:restaurant_id/relationships/dishes | {"_method":"PATCH","action":"relationships","controller":"Restaurants","plugin":null,"prefix":"Api","type":"Dishes"}            |
| api:restaurants:relationships       | /api/restaurants/:restaurant_id/relationships/dishes | {"_method":"POST","action":"relationships","controller":"Restaurants","plugin":null,"prefix":"Api","type":"Dishes"}             |
| api:restaurants:relationships       | /api/restaurants/:restaurant_id/relationships/dishes | {"_method":"DELETE","action":"relationships","controller":"Restaurants","plugin":null,"prefix":"Api","type":"Dishes"}           |
| api:restaurants:relationships       | /api/restaurants/:restaurant_id/relationships/dishes | {"_method":"GET","action":"relationships","controller":"Restaurants","plugin":null,"prefix":"Api","type":"Dishes"}              |
| CrudJsonApi.Restaurants:Dishes      | /api/restaurants/:restaurant_id/dishes               | {"_method":"GET","action":"index","controller":"Dishes","from":"Restaurants","plugin":null,"prefix":"Api","type":"Dishes"}      |
| api:dishes:index                    | /api/dishes                                          | {"_method":"GET","action":"index","controller":"Dishes","plugin":null,"prefix":"Api"}                                           |
| api:dishes:add                      | /api/dishes                                          | {"_method":"POST","action":"add","controller":"Dishes","plugin":null,"prefix":"Api"}                                            |
| api:dishes:view                     | /api/dishes/:id                                      | {"_method":"GET","action":"view","controller":"Dishes","plugin":null,"prefix":"Api"}                                            |
| api:dishes:edit                     | /api/dishes/:id                                      | {"_method":["PUT","PATCH"],"action":"edit","controller":"Dishes","plugin":null,"prefix":"Api"}                                  |
| api:dishes:delete                   | /api/dishes/:id                                      | {"_method":"DELETE","action":"delete","controller":"Dishes","plugin":null,"prefix":"Api"}                                       |
| api:dishes:relationships            | /api/dishes/:dish_id/relationships/restaurants       | {"_method":"PATCH","action":"relationships","controller":"Dishes","plugin":null,"prefix":"Api","type":"Restaurants"}            |
| api:dishes:relationships            | /api/dishes/:dish_id/relationships/restaurants       | {"_method":"POST","action":"relationships","controller":"Dishes","plugin":null,"prefix":"Api","type":"Restaurants"}             |
| api:dishes:relationships            | /api/dishes/:dish_id/relationships/restaurants       | {"_method":"DELETE","action":"relationships","controller":"Dishes","plugin":null,"prefix":"Api","type":"Restaurants"}           |
| api:dishes:relationships            | /api/dishes/:dish_id/relationships/restaurants       | {"_method":"GET","action":"relationships","controller":"Dishes","plugin":null,"prefix":"Api","type":"Restaurants"}              |
| CrudJsonApi.Dishes:Restaurants      | /api/dishes/:dish_id/restaurants                     | {"_method":"GET","action":"index","controller":"Restaurants","from":"Dishes","plugin":null,"prefix":"Api","type":"Restaurants"} |
| api:settings:index                  | /api/settings                                        | {"_method":"GET","action":"index","controller":"Settings","plugin":null,"prefix":"Api"}                                         |
| api:settings:add                    | /api/settings                                        | {"_method":"POST","action":"add","controller":"Settings","plugin":null,"prefix":"Api"}                                          |
| api:settings:index                  | /api/settings                                        | {"_method":"OPTIONS","action":"index","controller":"Settings","plugin":null,"prefix":"Api"}                                     |
| api:_controller:index               | /api/{controller}                                    | {"action":"index","plugin":null,"prefix":"Api"}                                                                                 |
| api:_controller:_action             | /api/{controller}/{action}/*                         | {"action":"index","plugin":null,"prefix":"Api"}                                                                                 |
| customer:pages:display              | /customer                                            | {"0":"home","action":"display","controller":"Pages","plugin":null,"prefix":"Customer"}                                          |
| customer:pages:display              | /customer/pages/*                                    | {"action":"display","controller":"Pages","plugin":null,"prefix":"Customer"}                                                     |
| customer:_controller:index          | /customer/{controller}                               | {"action":"index","plugin":null,"prefix":"Customer"}                                                                            |
| customer:_controller:_action        | /customer/{controller}/{action}/*                    | {"action":"index","plugin":null,"prefix":"Customer"}                                                                            |
| customer:_controller:_action        | /customer/{controller}/{action}                      | {"action":"index","plugin":null,"prefix":"Customer"}                                                                            |
| device/customer:pages:display       | /device/customer                                     | {"0":"home","action":"display","controller":"Pages","plugin":null,"prefix":"Device\/customer"}                                  |
| device/customer:pages:display       | /device/customer/pages/*                             | {"action":"display","controller":"Pages","plugin":null,"prefix":"Device\/customer"}                                             |
| device/customer:_controller:index   | /device/customer/{controller}                        | {"action":"index","plugin":null,"prefix":"Device\/customer"}                                                                    |
| device/customer:_controller:_action | /device/customer/{controller}/{action}/*             | {"action":"index","plugin":null,"prefix":"Device\/customer"}                                                                    |
| quasaradmin._controller:index       | /quasar-admin/{controller}                           | {"action":"index","plugin":"QuasarAdmin"}                                                                                       |
| quasaradmin._controller:_action     | /quasar-admin/{controller}/{action}/*                | {"action":"index","plugin":"QuasarAdmin"}                                                                                       |
| quasaradmin._controller:index       | /quasar-admin/{controller}                           | {"action":"index","plugin":"QuasarAdmin"}                                                                                       |
| quasaradmin._controller:_action     | /quasar-admin/{controller}/{action}/*                | {"action":"index","plugin":"QuasarAdmin"}                                                                                       |
+-------------------------------------+------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------+
geoidesic commented 4 years ago

btw I’ve updated the OP with extra notes, highlighting what I think is a second problem with links.