laravel-json-api / laravel

JSON:API for Laravel applications
MIT License
541 stars 41 forks source link

Singular not working on hasMany relation #198

Closed timyourivh closed 2 years ago

timyourivh commented 2 years ago

When I have 2 entities, for example: group and member:

Group ```php public function fields(): array { return [ ID::make(), Str::make('name'), Str::make('slug'), DateTime::make('created_at')->sortable()->readOnly(), DateTime::make('updated_at')->sortable()->readOnly(), // Relations HasMany::make('mambers')->canCount(), ]; } public function filters(): array { return [ WhereIdIn::make($this), Where::make('slug')->singular(), ]; } ```
Member ```php public function fields(): array { return [ ID::make()->uuid(), Str::make('name'), Str::make('identifier'), Str::make('data'), DateTime::make('created_at')->sortable()->readOnly(), DateTime::make('updated_at')->sortable()->readOnly(), // Relations BelongsTo::make('group') ]; } public function filters(): array { return [ WhereIdIn::make($this), Where::make('identifier')->singular(), ]; } ```

If I want to query a member of a group by it's unique and singular identifier I still get an array as if it wasn't singular.

To elaborate:

Works fine:

GET http://localhost/api/v1/members?filter[identifier]=JG5RUJUJ HTTP/1.1

Result

{
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/api/v1/members/3a27001d-ab68-461c-ab88-f2a60066c236"
  },
  "data": {
    "type": "members",
    "id": "3a27001d-ab68-461c-ab88-f2a60066c236",
    "attributes": {
      "name": "Test",
      "identifier": "JG5RUJUJ",
      "data": null,
      "created_at": "2022-05-20T11:19:00.000000Z",
      "updated_at": "2022-05-20T11:19:00.000000Z"
    },
    "relationships": {
      "group": {
        "links": {
          "related": "/api/v1/members/3a27001d-ab68-461c-ab88-f2a60066c236/group",
          "self": "/api/v1/members/3a27001d-ab68-461c-ab88-f2a60066c236/relationships/group"
        }
      }
    },
    "links": {
      "self": "/api/v1/members/3a27001d-ab68-461c-ab88-f2a60066c236"
    }
  }
}

Doesn't return singular

GET http://localhost/api/v1/group/5b6b94e2-dfcb-4650-a9b4-bc3d1cc28cdb/members?filter[identifier]=JG5RUJUJ HTTP/1.1

Returns

{
  "meta": {
    "count": 2
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "related": "/api/v1/groups/5b6b94e2-dfcb-4650-a9b4-bc3d1cc28cdb/members",
    "self": "/api/v1/groups/5b6b94e2-dfcb-4650-a9b4-bc3d1cc28cdb/relationships/members"
  },
  "data": [  <-- returns array
    {
      "type": "members",
      "id": "3a27001d-ab68-461c-ab88-f2a60066c236",
      "attributes": {
        "name": "Test",
        "identifier": "JG5RUJUJ",
        "data": null,
        "created_at": "2022-05-20T11:19:00.000000Z",
        "updated_at": "2022-05-20T11:19:00.000000Z"
      },
      "relationships": {
        "group": {
          "links": {
            "related": "/api/v1/members/3a27001d-ab68-461c-ab88-f2a60066c236/group",
            "self": "/api/v1/members/3a27001d-ab68-461c-ab88-f2a60066c236/relationships/group"
          }
        }
      },
      "links": {
        "self": "/api/v1/members/3a27001d-ab68-461c-ab88-f2a60066c236"
      }
    }
  ]
}

I've also tried defining the filter like this:

HasMany::make('clients')->canCount()->withFilters(
    Where::make('identifier')->singular(),
),

Which returns the same as above

steven-fox commented 2 years ago

The docs state this will be the case: https://github.com/laravel-json-api/docs/blob/eb296a3efa759401eb9924e4136f6659f3f1f8f6/2.0/schemas/filters.md?plain=1#L842

Thus, I wouldn't consider this a bug, but perhaps a discussion could be had to express why this is the case.

Personally, I feel like the singular filters feature will typically add complexity to the client consuming the api (needing to know which situations will lead to an array vs. object/null response), and I'd rather it be consistent 100% of the time. If you need a singular/null response, you should hit the resource's show route with the route key. Otherwise, if you're hitting the index (or relation index) routes, you'll always get a collection response. I actually feel not doing this goes against the json:api specification, but perhaps that's just me.

timyourivh commented 2 years ago

I'll just stick with if (!!data[0]) { ... } then.

lindyhopchris commented 2 years ago

Hi! yes this is intentionally, because the JSON:API spec defines that a has-many relation will be an array of resources. Which is why I designed it to ignore the singular filters on has-many relationships.

For the index route of a resource, the spec isn't restrictive over whether data is an array or a resource, which is why the singular resources are an option for developers to use there.

timyourivh commented 2 years ago

@lindyhopchris Is there a way to enable this behavior anyway? Because I would prefer to have a singular response when defined and would make my client code look cleaner.

lindyhopchris commented 2 years ago

@timyourivh at the moment there's no way to enable it. I'm dubious whether it's compliant with the spec.

Reading the spec, it definitely isn't allowed for the relationship endpoint e.g. /api/v1/posts/123/relationships/comments. It's not allowed because the spec says the response must be a relationship linkage which is defined as an array of identifiers for a to-many relationship.

The spec is unclear whether it's allowed for the related resources endpoint, e.g. /api/v1/posts/123/comments.