Select Page

Relations between documents can be a pretty important part of an AODocs library implementation. When aiming to enforce cardinality or even make those relations conditionally available, a bit of extra work is required and some understanding of when custom scripts get triggered is necessary.

In this article, we will illustrate what’s possible by creating a library centered around one use case. We will be modeling a media library meant for marketing purposes, leading to our first class being simply called “Image”. While most images are created internally and free for our marketing employees to use, some are available under specific licenses. We will mark those images by setting their boolean field “Under license” to true. When this boolean is set to true, they must now have a relation to exactly one document of our second type called “License”. The relation is called “Image > License” and a License can be tied to any number of Images.

 

Scripts Targeting Relations

The first reflex is to look for types of custom scripts that are directly tied to relations, namely “Relation Render Action” and “Relation Filter”.

While the former is just meant to change how relations are displayed, the latter looks promising at first glance.

Adding a Relation Filter script with just a simple

return new ArrayList<ReadableDocument>();

on the “To Filter action” side of the “Image > License” relation prevents users from picking any existing License to add to the relation.

Building upon this approach, we could block if our current Image already has a License attached to it:

if(!document.getToRelatedDocumentIds("Image > License").isEmpty()) {
  return new ArrayList<ReadableDocument>();
}
return relatedDocuments;

This script would need to be paired with a slightly more complex script to control the other side of the relationship (i.e., from the License side, attach an Image). That script would automatically filter out Images that already have a License:

List<ReadableDocument> eligibleDocs = new ArrayList<>();
for (ReadableDocument relatedDoc : relatedDocuments) {
  if (relatedDoc.getToRelatedDocumentIds("Image > License").isEmpty()) {
    eligibleDocs.add(relatedDoc);
  }
}
return eligibleDocs;

This approach still falls short of achieving our goals. For example, it doesn’t prevent an Image with no License to pick two of them at the same time to add to its related documents. It also allows an Image with a License already attached to a attach a second one by creating it on the fly, even though we forced the list of existing Licenses to be empty:

Finally, it would also not prevent an Image marked as “Under license” to be saved without any License attached.

As such, while Relation Filters can be convenient to prevent a distracted user from adding related documents in some scenarios, it’s not enough to enforce cardinality, whether conditional or not.

 

Update Action Triggers

Due to the limitations of the other options, Update Actions become the only way to achieve our goals. So we need to get a better understanding of when they get triggered, especially when it comes to adding and removing related items.

There are 3 ways to add related items:

  1. From the view page, link an existing document
    When viewing a document, each related class has a “+” icon next to its name. The window that it opens has two parts, and the top part allows you to search for and select several existing documents to relate at once.
  2. From the view page, create a new item to relate
    This is the option selected in the screenshot in the previous section of this post. It will then follow the normal rules of document creation where, after selecting a document and picking its title, it will either straight up create it (with the related item set) if there are no mandatory properties, or will redirect us to an edit screen pre-filled with the related item and allow us to set the mandatory properties and save.
  3. From the edit page, add and remove related items
    If you first edit the document, you will still have the “+” icon next to class names, but that will only allow you to pick existing documents and not to create one on the fly.

    This is the only screen that will allow you to un-link existing related documents.

Note: In the following, the document you trigger the action on is the source and is unrelated to the direction of the relation. The target is then the other end of the relation.

When the first way is used, clicking “add selected items” will automatically trigger the Update Actions tied to the document you were viewing. For example, if viewing an Image and adding a related License via this method, Update Actions tied to the Image will be triggered, but not Update Actions tied to the License, even though its related documents were changed.

When the second way is used, Update Actions will only be triggered on the target document. So, when viewing an Image and linking a new License by creating it on the fly, Update Actions will only trigger on the License and not on the Image.

When the third way is used, it is possible to add and remove related items and the Update Actions will only trigger once the save button is pressed, saving all of the changes at once. However, the Update Actions will only trigger on the source document. This means that editing an Image to unlink its current related License and link a different one instead will only trigger Update Actions on the Image and not on either of the old and new Licenses.

 

Implementation

Back to our initial use case: Images under license must be related to exactly one License, Images that aren’t under license must not be related to any License. We do not care how many Images each License is related to, if any.

If a user tries to make any modification that would go against this cardinality, we throw an exception that is displayed to the user and prevents the save.

 

In order to enforce the above, we need to check cardinality when:

    1. A new Image is created
      This takes care of both cases where an Imageis created directly via the New button, or whether it’s created by a License’s “create a new item to relate” action.
    2. An Image is updated and its “Under License” property is changed or its related Licenses are changed
    3. A new License is created
      This takes care of both cases where a License is created directly via the New button, or whether it’s created by an Image’s “create a new item to relate” action.In this case, we can directly throw if any Image is attached because it would mean one of the following scenarios:
      A. The License was created through the “New” button, and is trying to add a relation with an existing Image. We can assume that the existing Image was already in a stable state (either not under license and no related License, or under license and a single related License) hence adding an extra related License would always be invalid.

      B. The License was created by the “create a new item to relate” action from the view page of an Image. Similarly, we can assume that any Image we’re on the “view” page of is stable hence adding a new License to it is always wrong.

    4. A License is updated
      In this case we want to check that no Image relation was changed through this update. Similar to 3, we can assume that all Images are in a stable state, hence adding or removing a related License would result in an invalid amount of related Licenses.
    5. A License is deleted
      If a user deleted a License that is actively used by an Image under license, that Image would still be marked Under License but have no linked License. While in some scenarios it would make sense to cascade the deletion (e.g. ending an agreement with a third party), we will play it safe and prevent it altogether.

To achieve this, we need two scripts. One for Images that will handle the first 2 scenarios, and a second one for Licenses that will handle the last 3.

Here is what the Update Action for Images would look like:

boolean isUnderLicense =
    (Boolean) imageFields.getFieldByName("Under License").getValue();
if (isUnderLicense) {
  if (relatedLicenses.size() != 1) {
    throw new ValidationException(
        "An Image under license must be related to exactly one License.");
  }
} else {
  if (!relatedLicenses.isEmpty()) {
    throw new ValidationException(
        "An Image that isn't under license should not be related to any License.");
  }
}

And here is an Update Action for Licenses that would achieve our goals:

List<String> relatedImages = document.getFromRelatedDocumentIds("Image > License");

if (document.isDeleted() && !relatedImages.isEmpty()) {
  throw new ValidationException("You can't delete a License with related Images.");
} else {
  if (document.isNew()) {
    if (!relatedImages.isEmpty()) {
      throw new ValidationException(
          "You can't attach Images from the License side of the relation.");
    }
  } else {
    ReadableDocument originalLicense = document.getOriginalDocument();
    List<String> originalRelatedImages =
        originalLicense.getFromRelatedDocumentIds("Image > License");
    // In order to compare both lists while ignoring the order and duplicates,
    // we use sets.
    if (!new HashSet<>(relatedImages).equals(new HashSet<>(originalRelatedImages))) {
      throw new ValidationException(
          "You can't modify related Images from the License side of the relation.");
    }
  }
}

Note: While it is not the case for our example above, there might be a scenario where you want to enforce that exactly one License is attached to each Image no matter what. If this is the case, and that your Image does not have any mandatory properties, you won’t be able to create an Image via the “New” button as it would save automatically without giving you a chance to pick a related License. If such a scenario occurs, you might want to make sure that you have at least one mandatory property on Images, or your only way to create one will be through the “create a new item to relate” action on Licenses.

 

Conclusion

While not immediately straightforward, it is possible to enforce cardinality and add advanced conditions around document relations. This allows advanced browsing through content while also ensuring that such content is always linked, if and only if it is relevant. If you have questions, or need any additional information, do not hesitate to contact us.

Pin It on Pinterest

Sharing is caring

Share this post with your friends!