crnk-project / crnk-framework

JSON API library for Java
Apache License 2.0
286 stars 153 forks source link

2 query parameters 'fields' on different resources but on the same set of fields cancel the last one #856

Open lgringo opened 1 year ago

lgringo commented 1 year ago

Issue

Given a resource named main having two related resources of type a and b. When requesting main using fields[a]=label and fields[b]=label Result all fields of resource b are returned. Expected only label must be returned.

Example :

Resources


@JsonApiResource(type = "main")
public class MainResource {

    @JsonApiId
    private String id;

    @JsonApiRelation
    private RelatedResourceA relatedResourceA;

    @JsonApiRelation
    private RelatedResourceB relatedResourceB;
}

@JsonApiResource(type = "a")
public class RelatedResourceA {
    @JsonApiId
    private String id;

    private String label;

    private String otherField;
}

@JsonApiResource(type = "b")
public class RelatedResourceB {
    @JsonApiId
    private String id;

    private String label;

    private String somethingDifferent;
}

NB: Getters and setters are omitted for brevity.

Requests

NB: requests are written with httpie, for these examples, you just have to know that 'x==y' means add a request parameter x with value y.

No fields :+1:

{
    "data": {
        "id": "mainId",
        "links": " [...] "
        "relationships": {
            "relatedResourceA": {
                "data": {
                    "id": "mainId.A",
                    "type": "a"
                },
                "links": " [...] "
            },
            "relatedResourceB": {
                "data": {
                    "id": "mainId.B",
                    "type": "b"
                },
                "links": " [...] "
            }
        },
        "type": "main"
    },
    "included": [
        {
            "attributes": {
                "label": "Label from resource A",
                "otherField": "Other"
            },
            "id": "mainId.A",
            "links": " [...] ",
            "type": "a"
        },
        {
            "attributes": {
                "label": "Label from resource B",
                "somethingDifferent": "Other"
            },
            "id": "mainId.B",
            "links": " [...] ",
            "type": "b"
        }
    ],
    "links": " [...] "
}

All fields from resource A and resource B are returned.

Only label for resource A (or resource B) :+1:

{
...
    "included": [
        {
            "attributes": {
                "label": "Label from resource A"
            },
            "id": "mainId.A",
            "links": " [...] ",
            "type": "a"
        },
        {
            "attributes": {
                "label": "Label from resource B",
                "somethingDifferent": "Other"
            },
            "id": "mainId.B",
            "links": " [...] ",
            "type": "b"
        }
    ],
...
}

Field otherField has been filtered out.

Only label for resource A and resource B :-1:

{
...
    "included": [
        {
            "attributes": {
                "label": "Label from resource A"
            },
            "id": "mainId.A",
            "links": " [...] ",
            "type": "a"
        },
        {
            "attributes": {
                "label": "Label from resource B",
                "somethingDifferent": "Other"
            },
            "id": "mainId.B",
            "links": " [...] ",
            "type": "b"
        }
    ],
...
}

The field 'somethingDifferent' is returned when it shouldn't

Here the full spring boot app to illustrate You need maven and java (8+), unzip, go to directory, run mvn spring-boot:run

Causes :

QuerySpec. getNestedSpecs returns a Set of QuerySpec

    public Collection<QuerySpec> getNestedSpecs() {
        // Using a set to remove duplicate querySpec between typeRelatedSpecs and classRelatedSpecs
        Set<QuerySpec> allRelatedSpecs = new HashSet(typeRelatedSpecs.values());
        allRelatedSpecs.addAll(classRelatedSpecs.values());
        return Collections.unmodifiableCollection(allRelatedSpecs);
    }

and

QuerySpec.equals does not use resourceType (neither resourceClass).

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        QuerySpec other = (QuerySpec) obj;
        return CompareUtils.isEquals(filters, other.filters) // NOSONAR
                && CompareUtils.isEquals(includedFields, other.includedFields)
                && CompareUtils.isEquals(includedRelations, other.includedRelations)
                && CompareUtils.isEquals(pagingSpec, other.pagingSpec)
                && CompareUtils.isEquals(typeRelatedSpecs, other.typeRelatedSpecs)
                && CompareUtils.isEquals(classRelatedSpecs, other.classRelatedSpecs)
                && CompareUtils.isEquals(sort, other.sort);
    }

So when DocumentMapperUtil.getRequestedFields is called, QuerySpecAdapter.getIncludedFields is called, and then QuerySpec.getNestedSpecs is called ... and a spec is missing.