Handle multiple N:N records associate and disassociate requests with JavaScript and Power Pages Web API

In my previous posts Creating and removing N:N relationship between Dataverse records using Javascript and Power Pages Web API I showed how we can leverage the Power Pages Web API to handle single N:N associate and disassociate requests for a custom N:N relationship between Accounts and Contacts, and also in the following post Generic JavaScript Functions to Associate and Disassociate Dataverse records using the Power Pages Web API, I brought revisited versions of functions to handle the Associate and Disassociate process for any table.

In case we want to handle multiple requests, we need to develop some extra handling and implement function calling for arrays of IDs.

In this post I will bring a new function to handle multiple requests.

Prerequisites

  • All required fields (including the N:N relationship name) in the code must be enabled for the WebAPI for the Power Pages site where you wish to run the code creation via Web API
  • The WebAPI Wrapper code must be added to the page you are using the JavaScript code
  • An N:N relationship between two tables you want to handle programmatically needs to be configured in Dataverse
  • Include the generic functions AssociateRecords and DisassociateRecords from my previous post

Handle Multiple Disassociate and Associate Requests

The HandleMultipleAssociateDisassociate function uses recursion to handle multiple associate and disassociate operations. The gist of the idea is, the function receives an object containing arrays of ids to be processed, it will process one item, and call itself again with the remaining ids to be processed (excluding the one just processed – either to associate or disassociate).

The function takes in an object multipleRequestData as a parameter.

This object contains information about the records to associate and disassociate, as well as callback functions to execute on success and error.

The format of the multipleRequestData object is as follows:

  • idsToAssociate: An array of record IDs to associate (example, array of contactid values)
  • idsToDisassociate: An array of record IDs to disassociate (example, array of contactid values)
  • recordTable: The plural internal name of the record table to use for the association and disassociation operations (example contact)
  • relatedRecordId: The ID of the related record to associate or disassociate (for example accountid)
  • relatedRecordTable: The plural internal name of the related record table to use for the association and disassociation operations (for example: account)
  • relationshipSchemaName: The name of the relationship schema to use for the association and disassociation operations (for example: prefix_Account_Contact_Contact)
  • onAssociateError: A callback function to execute on error in any Associate call.
  • onDisassociateError: A callback function to execute on error in any Disassociate call
  • runAfterAllSuceeded: A callback function to execute after all associate and disassociate operations have completed.

The function then checks if there are any record IDs left to associate or disassociate by calling the pop method on the idsToAssociate and idsToDisassociate arrays, respectively.

  • If there is an ID to left associate, the function calls the AssociateRecords function with the appropriate parameters and passes in a success callback function that then calls the HandleMultipleAssociateDisassociate function with a new multipleRequestData object (with updated arrays without the item just added, as the pop function removed it). This recursive call allows the function to continue processing the remaining record IDs to associate.
  • If there is no ID to associate but there is an ID to disassociate, the function calls the DisassociateRecords function with the appropriate parameters similarly to the function to associate, then calls the HandleMultipleAssociateDisassociate function again with the an updated multipleRequestData object (without the item just removed). The recursion works on the same way as for the AssociateRecords function call.
  • If there are no more IDs to associate or disassociate, the function checks if there is a runAfterAllSuceeded property in the multipleRequestData object and, if so, calls the function specified by this property.
function HandleMultipleAssociateDisassociate(multipleRequestData) 
{   
    let idToAssociate =  multipleRequestData.idsToAssociate ? multipleRequestData.idsToAssociate.pop() : null;
    let idToDisassociate = idToAssociate ? null : (multipleRequestData.idsToDisassociate ? multipleRequestData.idsToDisassociate.pop() : null);
    
    if (idToAssociate) {
        let thisRecord = {
            recordId: idToAssociate,
            recordTable: multipleRequestData.recordTable,
            relatedRecordId: multipleRequestData.relatedRecordId,
            relatedRecordTable: multipleRequestData.relatedRecordTable};

        AssociateRecords({
            ...thisRecord,
            relationshipSchemaName: multipleRequestData.relationshipSchemaName,
            onSucess: function (data, textStatus, xhr) {
                console.log(`Associated ${multipleRequestData.recordTable} ${idToAssociate} to ${multipleRequestData.relatedRecordTable} ${multipleRequestData.relatedRecordId}`);
                HandleMultipleAssociateDisassociate(multipleRequestData);
            },
            onError: function (xhr, textStatus, errorThrown) {
                multipleRequestData.onAssociateError(xhr, textStatus, errorThrown, thisRecord);
            }
        })     
    } else if (idToDisassociate) {
        let thisRecord = {
            recordId: idToDisassociate,
            recordTable: multipleRequestData.recordTable,
            relatedRecordId: multipleRequestData.relatedRecordId,
            relatedRecordTable: multipleRequestData.relatedRecordTable,
        }

        DisassociateRecords({
            ...thisRecord,
            relationshipSchemaName: multipleRequestData.relationshipSchemaName,
            onSucess: function (data, textStatus, xhr) {
                console.log(`Disassociated ${multipleRequestData.recordTable} ${idToDisassociate} from ${multipleRequestData.relatedRecordTable} ${multipleRequestData.relatedRecordId}`)
                HandleMultipleAssociateDisassociate(multipleRequestData);
            },
            onError: function (xhr, textStatus, errorThrown) {
                multipleRequestData.onDisassociateError(xhr, textStatus, errorThrown, thisRecord);
            }
        });       
    } else {
        if(multipleRequestData.runAfterAllSuceeded) { multipleRequestData.runAfterAllSuceeded() };
    }
}

Handling Errors

In this sample, when errors happen we are simply alerting the errors, but you can run some actions inside of this function to flag items that were associated/disassociated and maybe retry failed actions (you will need to implement maybe a ‘processing queue log’ in combination to the HandleMultipleAssociateDisassociate function above):

function onWebAPIAssociateError  (xhr, textStatus, errorThrown, objectData) {        
       alert(`${textStatus} error while saving data`, `Error associating ${objectData.recordTable} record (${objectData.recordId}) to ${objectData.relatedRecordTable} record (${objectData.relatedRecordId}) : ${xhr.responseJSON.error.message}`);
        console.log(xhr);       
}
function onWebAPIDisassociateError(xhr, textStatus, errorThrown, objectData) {       
       alert(`${textStatus} error while saving data`, `Error disassociating ${objectData.recordTable} record (${objectData.recordId}) to ${objectData.relatedRecordTable} record (${objectData.relatedRecordId}) : ${xhr.responseJSON.error.message}`);     
}

Sample function call

The idsToAssociate and idsToDisassociate properties need to be in Array format. If you obtained them as collections (such as via jQuery DOM queries), you might need to convert them. Using the notation below simplifies this conversion.

Run a function call like the below, and the code will take care of handling your relationships😊

let runAfterAllSuceeded = function () {        
    alert("Assignments saved successfully!");
}
HandleMultipleAssociateDisassociate({
        idsToAssociate: [...usersToAdd],
        idsToDisassociate: [...usersToRemove],
        recordTable: "contacts",
        relatedRecordId: accountID,
        relatedRecordTable: "accounts",
        relationshipSchemaName: "prefix_Account_Contact_Contact",
        runAfterAllSuceeded: runAfterAllSuceeded,
        onAssociateError: onWebAPIAssociateError,
        onDisassociateError: onWebAPIDisassociateError
});

In summary:

The HandleMultipleAssociateDisassociate function uses recursion to process multiple associate and disassociate operations by calling itself with an updated multipleRequestData object until there are no more record IDs left to process.

As the bulk associate/disassociate requests are not handled as a single transaction, you will need to handle possible failed requests yourself by implementing the functions onWebAPIAssociateError / onWebAPIDisassociateError that you send as property of the object sent as parameter to this funcion.

References

Associate and disassociate tables by using the Web API – Microsoft Learn

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *