jmix-framework / jmix

Jmix framework
https://www.jmix.io
Apache License 2.0
693 stars 124 forks source link

REST composition update returns validation errors #3842

Open dvaschenko opened 1 week ago

dvaschenko commented 1 week ago

Environment

Jmix version: 2.4.0

Bug Description

While trying to update composite collection as described at https://docs.jmix.io/jmix/rest/entities-api/update-entities.html#composition-attributes - behaviour is completely wrong. Expeced - passed entyty id keeps this entity unchanged and as part of composition Actual - it is looking that there is an attempt to create new entity is being made every single time. Validation errors are always returned from API.

Steps To Reproduce

Use provided project for testing. Fetch plan is defined in project, GET token request should return valid token after start. User ID's are coorect as well, since they are hardcoded in app. Ater start of application make request for token: [source]

curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic dWtpbHh4em1rdDpSUGRVeHFNT2Z5' \
--data-urlencode 'grant_type=client_credentials'

Use recieved token for further requests Create project with tasks via rest request: [source]

curl --location 'http://localhost:8080/rest/entities/Project?responseFetchPlan=project-with-tasks-fetch-plan' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer VALID_TOKEN' \
--data '{
    "name":"Rest project with three tasks",
    "manager": {
        "id":"c0315747-094d-4e19-b445-383a051aad07"},
    "tasks": [
        {
            "name":"Composite task one",
            "assignee":{
                "id":"60885987-1b61-4247-94c7-dff348347f93"
            },
            "startDate":"2024-11-06T14:00:23.278633"
        },
        {
             "name":"Composite task two",
            "assignee":{
                "id":"60885987-1b61-4247-94c7-dff348347f93"
            },
            "startDate":"2024-11-11T14:00:23.278633"
        },
         {
            "name":"Composite task three",
            "assignee":{
                "id":"60885987-1b61-4247-94c7-dff348347f93"
            },
            "startDate":"2024-11-06T14:00:23.278633"
        }
    ]    
}'

Use identifiers from response and try to update name of one of tasks: [source,http]

curl --location --request PUT 'http://localhost:8080/rest/entities/Project/VALID_PROJECT_ID?responseFetchPlan=project-with-tasks-fetch-plan' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer VALID_TOKEN' \

--data '{
   "tasks":[
       {"id":"VALID_TASK_1_ID"},
       {"id":"VALID_TASK_2_ID"},
       {
           "id":"VALID_TASK_3_ID",
           "name":"Renamed composite task three"
       }

   ]
}'

Current Behavior

Response from server is looking as follows:

[source,json]

[
    {
        "message": "may not be null",
        "messageTemplate": "{jakarta.validation.constraints.NotNull.message}",
        "path": "name",
        "invalidValue": null
    },
    {
        "message": "may not be null",
        "messageTemplate": "{jakarta.validation.constraints.NotNull.message}",
        "path": "name",
        "invalidValue": null
    }
]

So, it is obvious from the response that for first and second already existed tasks there was an attempt to create them once again, and due to the fact that name of a task is mandatory, the validation exception was thrown.

Expected Behavior

One of the task is renamed, and the tasks that were passed as simple ID's were not changed and are still present in composition, just as it is described in documentation

Sample Project

Product-Management-2_4_0.zip

dvaschenko commented 1 week ago

In addition, a couple of places that are looking problematic, IMHO

While composite entities are being created and then compared, the io.jmix.core.impl.EntityInternals.equals() is being used for some reason, and this method is only comparing the ID's of passed objects, nothing more. So, newly created instance in the end of importOneToManyCollectionAttribute(), that is .compare(collectionValue, prevCollectionValue); can make the comparison incorrect, since newly created entity has no attribute values.

Second place that puzzles me a lot - EntityImportExportImpl.importEntity() method, that at some point should import entity's local attributes using code:

EntityAttributeImportExtension extension = extensionResolver.findExtension(metaProperty);
            if (extension != null) {
                extension.importEntityAttribute(metaProperty, srcEntity, dstEntity);
                continue;
            }

In fact, EntityAttributeImportExtension is just an interface, that has no single implementation. Thus, it is unclear for me how local attributes can be imported in this case.