Revisions on relations are not loaded correctly resulting in wrong data in includes

Created on 16 October 2019, about 5 years ago
Updated 24 February 2023, almost 2 years ago

Problem/Motivation

Draft mode returns wrong revision of includes. I get default revision of second level paragraph instead of latest revision.

Technical problem:
When trying to access the working revision entity data with resourceVersion=rel:working-copy and using includes, the current revision of the referenced entities is being returned by the method "resolveIncludeTree" in IncludeResolver class. This method should return the data as per the revision being passed in the API query or the working revision.

Steps to reproduce

Install Content moderation;
Add support version negotiation for any entity type: "https://git.drupalcode.org/project/drupal/-/merge_requests/1365.patch"
Create paragraph type A;
Create paragraph type B;
Add paragraph reference field to paragraph A;
Create node with paragraph A (+ paragraph B inside A)
Publish node;
Create draft with changed B;
Check node json data with resourceVersion=rel:working-copy. Returned wrong revision of includes.

Proposed resolution

web/core/modules/jsonapi/src/IncludeResolver.php
loads included entity by id
$targeted_entities = $entity_storage->loadMultiple(array_unique($ids));

I suggest to load entities by revision id:
$targeted_entities = $entity_storage->loadMultipleRevisions(array_unique($revision_ids));

🐛 Bug report
Status

Needs work

Version

10.1

Component
JSON API 

Last updated 3 days ago

Created by

🇮🇳India neelam.chaudhary

Live updates comments and jobs are added and updated live.
  • Needs tests

    The change is currently missing an automated test that fails when run with the original code, and succeeds when the bug has been fixed.

  • Needs change record

    A change record needs to be drafted before an issue is committed. Note: Change records used to be called change notifications.

Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇮🇳India rassoni Bangalore

    #33 Re-roll against 10.x.1 branch. Applied successfully, but custom command failing. required NW

  • 🇮🇳India ameymudras

    I just had a look at #33 and I'm not sure why we are using

    +use Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem;
    
  • First commit to issue fork.
  • 🇦🇺Australia mstrelan

    Started to re-roll for 11.x but the changes to \Drupal\jsonapi\IncludeResolver::resolveIncludeTree from 🐛 ResourceTypeRepository wrongly assumes that all entity reference fields have the setting "target_type" Fixed have complicated this.

  • 🇦🇺Australia Nadim Hossain

    Re-rolled the patch against 10.2.x, still testing needs to be done on this re-rolled patch.

  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 8.1 & MySQL 5.7
    last update 12 months ago
    Custom Commands Failed
  • 🇦🇺Australia acbramley

    We can't reference classes from entity_reference_revisions in core, this needs a reroll starting from #21

  • 🇦🇺Australia Nadim Hossain

    Thanks @acbramley
    I have re-rolled it from #21 against 10.2.x

  • last update 12 months ago
    25,771 pass, 1,812 fail
  • 🇦🇺Australia mstrelan

    I don't think the re-roll is that simple. Now we're only adding to the revision_ids if the is_subclass_of condition fails.

  • 🇦🇺Australia mstrelan

    Opened MR against 11.x which is a re-roll of #21, but moves bits out of the is_subclass_of condition. This also applies to 10.2.x.

  • Pipeline finished with Failed
    12 months ago
    Total: 609s
    #73205
  • Pipeline finished with Success
    12 months ago
    Total: 732s
    #73239
  • 🇺🇸United States jberube

    As mentioned in #45, the patch for v10.2.x in #44 doesn't cover both conditions in the if/else. Using that patch, I wasn't seeing the results I expected, so I made this patch that covers both conditions, and now I see the results I expected.

  • 🇦🇺Australia acbramley

    Hiding patches to avoid bot confusion.

  • 🇬🇧United Kingdom jkdaza

    Patch #48 works for me, but

    list($target_type, $revision_type) = explode(':', $target_type_and_rev);
    

    assumes that the $target_type_and_rev will always have 2 values. This is not the case, so you end up with tons of warnings>
    I've changed it to

    list($target_type, $revision_type) = array_pad(explode(':', $target_type_and_rev), 2, NULL);
    

    and no more warnings.

    Attaching the revised patch.

  • Open in Jenkins → Open on Drupal.org →
    Environment: PHP 5.3 & MySQL 5.5
    last update 7 months ago
    Patch Failed to Apply
  • last update 7 months ago
    Patch Failed to Apply
  • 🇯🇴Jordan yahyaalhamad Palestine

    The line:

    list($target_type, $revision_type) = array_pad(explode(':', $target_type_and_rev), 2, NULL);
    

    throws a warning, I added '@' to suppress it since $revision_type is expected sometimes to be NULL.

    Applied on Core 10.2.X

  • First commit to issue fork.
  • 🇸🇪Sweden marcusml

    I rebased the MR and pushed a new commit with a different approach where I'm using the entity from the field item instead of loading it again from entity storage. Not sure if it has some downsides but it seems to work fine when testing manually.

  • Pipeline finished with Success
    6 months ago
    Total: 1215s
    #215168
  • Status changed to Needs review 3 months ago
  • 🇳🇱Netherlands bbrala Netherlands

    This approach seems great! Thanks for moving this forward.

    We do still need test. Probably best to look at JsonApiRegressionTest and then testDeepNestedIncludeMultiTargetEntityTypeFieldFromIssue2973681 which also creates some nested relations and tests those. It would just need to install content_moderation and make a forward revision.

  • 🇸🇪Sweden marcusml

    Awesome, thanks!

    Sorry for not addressing the needed tests. I'm not sure if test is needed since we're not changing any of core's behaviour, we're not expecting core's entity reference field to load a future draft(?) So it should already be covered by existing tests? My understanding here is that this allows modules like paragraphs to have a say in what revisions are loaded, but core should still works the same.

    I could be wrong and I'd be happy to try and write a test for this. But I'd need some guidance on how to approach it. 🙇

  • 🇳🇱Netherlands bbrala Netherlands

    DIsecting the change, you might actually be right. Instead of collecting ID's and then getting the entities we just reference the entity and create the list right away, doing away with the load multiple.

    Technically its the same, there is also no risk involved with not load multiple imo since we know we have the entiy already. I cannot see how there could ever be a way to have different entities loaded with loadMultiple than we already know of in the (validated for existing) entity property.

    This does assume the entity that is loaded is the correct revision, but that seems like ano brainer.

    I want to RTBC, but:

    1. IS needs update for new approach.
    2. Need a change record describing the change
  • 🇸🇪Sweden marcusml

    I've updated the proposed resolution. Assigning the issue to myself to draft a change record.

  • 🇸🇪Sweden marcusml

    I've drafted a change record now. I'm no technical writer and haven't authored one before but hope it helps some what to drive this issue forward. 🙂

  • 🇳🇱Netherlands bbrala Netherlands

    Thanks! I've updated the CR a little.

    I prefer to test this, ive put some time into this to see if i could test it with a kernel test. Seems i can use the resolver properly, just need to do the second level of node and see if i can reproduce this. I keep getting an itch this needs testing.

    Very rough work in progress.

    
    class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
    
      use ImageFieldCreationTrait;
      use ContentModerationTestTrait;
      use UserCreationTrait;
    
      public function testRelationshipVersionIncludes(){
    
        $this->assertTrue($this->container->get('module_installer')->install(['content_moderation'], TRUE), 'Installed modules.');
    
    
        $workflow = $this->createEditorialWorkflow();
        $this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'article');
        $this->type->getEntityType()->setHandlerClass('moderation', ModerationHandler::class);
    
    
        Role::create([
          'id' => RoleInterface::AUTHENTICATED_ID,
          'permissions' => [
            'access content',
            'view any unpublished content',
            'use editorial transition publish',
          ],
          'label' => 'Anonymous',
        ])->save();
    
        $this->setCurrentUser($this->user);
    
        $this->includeResolver = $this->container->get('jsonapi.include_resolver');
        $this->resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
    
        $this->createEntityReferenceField(
          'node',
          'article',
          'field_entity_reference',
          'Related entity',
          'node',
          'default',
          ['target_bundles' => []],
          1
        );
    
        $node = Node::create([
          'title' => 'original node',
          'type' => 'article',
          'uid' => $this->user,
          'body' => [
            'format' => 'plain_text',
            'value' => $this->randomString(),
          ],
          'field_tags' => [
            ['target_id' => $this->term1->id()],
            ['target_id' => $this->term2->id()],
          ],
          'field_image' => [
            [
              'target_id' => $this->file->id(),
              'alt' => 'test alt',
              'title' => 'test title',
              'width' => 10,
              'height' => 11,
            ],
          ],
        ]);
    
        $nodeWithReference = Node::create([
          'title' => 'original node with reference',
          'type' => 'article',
          'uid' => $this->user,
          'body' => [
            'format' => 'plain_text',
            'value' => $this->randomString(),
          ],
          'field_tags' => [
            ['target_id' => $this->term1->id()],
            ['target_id' => $this->term2->id()],
          ],
          'field_image' => [
            [
              'target_id' => $this->file->id(),
              'alt' => 'test alt',
              'title' => 'test title',
              'width' => 10,
              'height' => 11,
            ],
          ],
          'field_entity_reference' => [
            ['target_id' => $node->id(), 'entity' => $node],
          ],
        ]);
        $nodeWithReference->save();
    
        $node->set('title', 'updated node');
        $node->setNewRevision(TRUE);
        $node->set('moderation_state', 'draft');
        $node->save();
    
        $nodeWithReference->set('title', 'updated node with reference');
        $nodeWithReference->setNewRevision();
        $nodeWithReference->set('moderation_state', 'draft');
        $nodeWithReference->save();
    
        $parentVid = $nodeWithReference->get('vid')->value;
        $childVid = $node->get('vid')->value;
    
        $resource_type = $this->container->get('jsonapi.resource_type.repository')->get('node', 'article');
        $resource_object = ResourceObject::createFromEntity($resource_type, $nodeWithReference);
        $includes = $this->includeResolver->resolve($resource_object, 'field_entity_reference');
    
        $jsonapi_doc_object = $this
          ->getNormalizer()
          ->normalize(
            new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), $includes, new LinkCollection([])),
            'api_json',
            [
              'resource_type' => $resource_type,
              'account' => NULL,
              'include' => [
                'field_entity_reference',
              ],
            ]
          );
        $normalized = $jsonapi_doc_object->getNormalization();
    
    
    
        // Todo:
    
        // Node 1  (static) -> reference node 2 -> reference node 3
        // Update node 3 to have a working copy.
        // Ask for working copy. Might need
    
      }
    }
    
  • First commit to issue fork.
  • Pipeline finished with Success
    about 1 month ago
    Total: 416s
    #332717
  • Pipeline finished with Failed
    11 days ago
    Total: 504s
    #365161
  • Pipeline finished with Success
    11 days ago
    Total: 446s
    #365208
  • Pipeline finished with Success
    11 days ago
    Total: 548s
    #365221
  • Status changed to Needs work 11 days ago
  • 🇦🇺Australia silverham

    I worked on this during DrupalCon Singapore. I started work on a having mock entity reference revisions field type since entity reference revisions module is in contrib in git merge request. My test is just a mock placeholder. I am finished for today.

  • Pipeline finished with Failed
    11 days ago
    Total: 174s
    #365299
Production build 0.71.5 2024