Creating Relationships in Bulk

This source file includes a series of sample helper functions that completes the following scenario:

  • Start with an Array of Relationship Records.
    • We will assume that a preprocessor has already read the records from a file into an Array. In this example, we have stored the Array in a file for use.
  • Authenticate and Get Authorization Token.
  • Aggregate the Array of Relationship Records, referenced by Association ID, into a series of Create Relationship requests.
  • Extract the Association IDs from the Array of Relationship Records and retrieve the corresponding Asset Object IDs using the Asset Keyword Search REST API.
    • An Association ID to Object ID map will be cached for later reference.
  • Iterate over the Array of Create Relationship Requests to create Asset Relationships using the Create Relationship REST API.
    • Before creation, the Primary and Secondary Association IDs will be converted into Primary and Secondary Object IDs.
  • Any errors during processing will be logged to the console.

Application Configurations

This source file stores the configurations needed to run the example application.

Requirements

  • Replace the following variables in < > with real values:
    • endpointUrl
    • apiKey
    • clientId
    • username
    • password
    • association_id - The Association ID Search Field Name
  • Credentials and API Key are stored in a file here for simplicity, but should normally be stored in a secure location, such as in a Secrets Manager.

config/config.js

Copy
Copied
    const getApiConfig = () => {
        const endpointUrl = '<endpointUrl>';
        const apiKey = '<apiKey>';
        const config = {
            endpointUrl,
            piKey,
        };
        return config;
    };
    const getCredentials = () => {
        const clientId = '<clientId>';
        const username = '<username>';
        const password = '<password>';
        const credentials = {
            clientId,
            username,
            password,
        };
        return credentials;
    };
    const metadata = {
        ASSOCIATION_ID: {
        RECORD_NAMES: [
            'primaryAssociationId',
            'secondaryAssociationId',
        ],
        ATTRIBUTE_NAME: '<association_id>',
        },
    };
    module.exports = {
        getApiConfig,
        getCredentials,
        metadata,
    };

Sample Relationship Data

This source file stores sample Relationship data for simplicity. Normally, this data would be read from a spreadsheet, file, or other repository.

In this sample data, we point out potential reasons for errors, such as duplicate relationships, circular dependencies, invalid IDs, or invalid link types.

data/relationship.js

Copy
Copied
const createRelationshipRecords = [
 {
    primaryAssociationId: 'f47a3d97-eeed-47af-aed2-48c37dd8cbf9-1',
    secondaryAssociationId: '7f3d3d79-effc-4ecc-9259-74a99f0331da-2',
    linkType: 'placed_graphic',
    },
    {
    primaryAssociationId: 'f47a3d97-eeed-47af-aed2-48c37dd8cbf9-1',
    secondaryAssociationId: '92aa1ad3-e6d7-4676-a962-c70f277a28b8-2',
    linkType: 'child',
    },
    {
    // Circular Relationship
    primaryAssociationId: '92aa1ad3-e6d7-4676-a962-c70f277a28b8-2',
    secondaryAssociationId: 'f47a3d97-eeed-47af-aed2-48c37dd8cbf9-1',
    linkType: 'child',
    },
    {
    // Duplicate Relationship
    primaryAssociationId: 'd38c9c7a-efeb-400d-8b3f-222eb2395e5e-1',
    secondaryAssociationId: '48ced887-c2ea-402c-8fff-a5213a5f8efb-2',
    linkType: 'derivative',
    },
    {
    primaryAssociationId: 'd38c9c7a-efeb-400d-8b3f-222eb2395e5e-1',
    secondaryAssociationId: '92aa1ad3-e6d7-4676-a962-c70f277a28b8-2',
    // Invalid Link Type
    linkType: 'child',
    },
    {
    primaryAssociationId: 'd38c9c7a-efeb-400d-8b3f-222eb2395e5e-1',
    // Invalid Secondary Association ID
    secondaryAssociationId: '92aa1ad3-e6d7-4676-a962-c70f277a28b8-3',
    linkType: 'child',
    },
    {
    // Invalid Primary Association ID
    primaryAssociationId: 'f47a3d97-eeed-47af-aed2-48c37dd8cbf9-2',
    secondaryAssociationId: '3919382b-7b5b-4b6f-93af-8d36247cb68e-2',
    linkType: 'child',
    },
];

module.exports = {
 createRelationshipRecords,
};

Bulk Create Relationships

In this source file, we include the following helper functions:

  • aggregateAssocationRelationshipRecords - Aggregate an Array of Relationship Records into an Array of Create Relationship Requests.
    • The Relationship record includes a primaryAssociationId, secondaryAssociationId, and linkType.
    • The Create Relationship Request includes a primaryAssociationId and an Array of secondaryAssociationId / linkType entries, where each entry is a separate Relationship.
  • retrieveAssetsByAssociationId - Given an Array of Create Relationship Requests referenced by Association ID, retrieve the Assets corresponding to each Association ID, using the Asset Keyword Search API, and return an Association ID to Asset Object ID Map.
  • convertAssociationToAssetRelationships - Given an Array of Create Relationship Requests referenced by Association ID, convert the Association IDs to Asset Object IDs in each Request and return the modified Array.
  • createRelationships - Using the above helper functions, get Authorization data, convert the Relationship Records into an Array of Create Asset Relationship Requests, and process the Requests using the Create Relationship API. Log any errors during processing. During the Create Relationship API processing, errors may include Arrays of Relationship-specific errors or consolidated error messages that may occur during Request validation by the API.

helper/create-relationships.js

Copy
Copied
const _ = require('lodash');
const Config = require('../config/config');
const AuthenticationService = require('../service/authentication');
const AssetSearchService = require('../service/asset-search');
const RelationshipService = require('../service/relationship');
const RelationshipData = require('../data/relationship');
/**
* @description Aggregate a Tabular Array of Association ID-based Relationship Records into
*  an Array of Association ID-based Create Relationship Objects.
* @param {Array} records Array of Association ID-based Records
* @returns {Array} Array of Create Relationships by Association ID
*/
const aggregateAssociationRelationshipRecords = (records) => {
 // Validate Records
 if (!Array.isArray(records)) {
   throw new Error('Create Relationship Records must be an Array');
 }
 // Prepare Primary Association ID to Secondary Association IDs Relationship Map
 const relationshipMap = {};
 // Loop Over Records to Aggregate by Primary ID
 records.forEach((record) => {
   // Extract Association IDs and Link Type
   const { primaryAssociationId, secondaryAssociationId, linkType } = record;
   // Check If Primary Association ID Mapping Exists
   if (relationshipMap[primaryAssociationId]) {
     // Add Relationship to Existing Mapping List
     relationshipMap[primaryAssociationId].push({
       secondaryAssociationId,
       linkType,
     });
   } else {
     // Else Create new Relationship Mapping List
     relationshipMap[primaryAssociationId] = [
       {
         secondaryAssociationId,
         linkType,
       },
     ];
   }
 });
 // Convert Mapping to Array of Create Relationship Entries
 const associationRelationships = [];
 Object.keys(relationshipMap).forEach((primaryAssociationId) => {
   associationRelationships.push({
     primaryAssociationId,
     secondaryAssociations: relationshipMap[primaryAssociationId],
   });
 });
 // Return Array of Create Relationship Records using Association IDs
 return associationRelationships;
};
/**
* @description Retrieve Assets given an Array of Association IDs.
*  Generate and Return an Association ID to Asset Object ID Map.
* @param {{records, associationIdNames, requestContext}} request
* @returns {*} Association ID to Object ID Map
*/
const retrieveAssetsByAssociationId = async (request) => {
 const { records, associationIdNames, requestContext } = request;
 const associationIdObjectIdMap = {};
 let searchTerms = [];
 // Validate Parameters
 if (!Array.isArray(records)) {
   throw new Error('records must be an Array');
 }
 if (!Array.isArray(associationIdNames) && associationIdNames) {
   throw new Error('associationIdNames must be an Array');
 }
 if (!requestContext) {
   throw new Error('Missing options.requestContext');
 }
 // Loop to Extract Association IDs from Records
 records.forEach((record, i) => {
   // For Each Record, Extract the Association ID Values
   associationIdNames.forEach((name) => {
     const value = record[name];
     // Value Must be Populated
     if (!value) {
       throw new Error(`Missing [${name}] value in Record ${i}`);
     }
     // Save Association ID as Search Term
     searchTerms.push(value);
   });
 });
 // Remove Duplicate Association IDs
 searchTerms = _.uniq(searchTerms);
 // Chunk Assets into Batches
 const batchSize = 100;
 const searchTermChunks = _.chunk(searchTerms, batchSize);
 // Get Association ID Search Field Name
 const associationIdFieldName = Config.metadata.ASSOCIATION_ID.ATTRIBUTE_NAME;
 // Search for Assets by Association ID
 for (let i = 0; i < searchTermChunks.length; i += 1) {
   const searchTermChunk = searchTermChunks[i];
   // Prepare Search Request
   const search = {
     searchTerm: searchTermChunk,
     limit: batchSize,
     operation: 'OR',
   };
   const searchRequest = {
     ...requestContext,
     search,
   };
   // Run Asset Search
   // eslint-disable-next-line no-await-in-loop
   const searchResult = await AssetSearchService.runKeywordSearch(searchRequest);
   // Extract Association ID Values
   for (let j = 0; j < searchResult.result.length; j += 1) {
     const asset = searchResult.result[j];
     const { objectId } = asset;
     const metadataValue = _.get(asset, `metadataDenormalized[${associationIdFieldName}]`);
     // Add Association ID / Object ID Mapping If Not Not Exist
     if (metadataValue && !associationIdObjectIdMap[metadataValue]) {
       associationIdObjectIdMap[metadataValue] = objectId;
     }
   }
 }
 console.log('Association ID / Object ID Map:', associationIdObjectIdMap);
 // Return Association ID / Object ID Map
 return associationIdObjectIdMap;
};
/**
* @description Convert an Array of Association ID-based Relationships into an Array of Asset Relationships.
*  Request Context is included to support the authorized calls to the REST API.
* @param {{associationRelationships, requestContext}} request
* @returns {Array} Array of Asset Relationships
*/
const convertAssociationToAssetRelationships = async (request) => {
 const { associationRelationships, associationIdObjectIdMap } = request;
 const newRelationships = [];
 // Validate Association Relationships Array
 if (!Array.isArray(associationRelationships)) {
   throw new Error('Association Relationships must be an Array');
 }
 // Loop Over Association Relationships to convert to Asset Relationship
 for (let i = 0; i < associationRelationships.length; i += 1) {
   const associationRelationship = associationRelationships[i];
   const { primaryAssociationId, secondaryAssociations } = associationRelationship;
   try {
     // Convert Primary Association ID to Object ID
     const primaryId = associationIdObjectIdMap[primaryAssociationId];
     // Validate Primary ID
     if (!primaryId) {
       throw new Error('Primary Asset Not Found by Association ID:', {
         primaryAssociationId,
       });
     }
     // Prepare New Secondary Relationships
     const secondaryIds = [];
     // Loop Over Secondary Associations to Convert to Asset Relationships
     for (let j = 0; j < secondaryAssociations.length; j += 1) {
       const secondaryAssociation = secondaryAssociations[j];
       try {
         // Prepare Secondary Association ID Asset Search Request
         const { secondaryAssociationId } = secondaryAssociation;
         // Get Secondary Object ID
         const secondaryObjectId = associationIdObjectIdMap[secondaryAssociationId];
         // Validate Secondary Object ID
         if (!secondaryObjectId) {
           throw new Error('Secondary Asset Not Found by Association ID for Primary Asset Relationship:', {
             primaryId,
             secondaryAssociationId,
           });
         }
         // Add New Secondary ID Entry
         const secondaryId = {
           // id: objectId,
           id: secondaryObjectId,
           linkType: secondaryAssociation.linkType,
         };
         secondaryIds.push(secondaryId);
       } catch (error) {
         console.error(
           'Failed to Locate Secondary Association Assets for Relationship Conversion:',
           error.message,
           {
             primaryAssociationId,
             secondaryAssociation,
           },
           error.stack,
         );
       }
     }
     // Check for Relationships to Create
     if (!secondaryIds.length) {
       throw new Error('No Secondary IDs generated to create new Asset Relationship');
     }
     // Add Asset Relationship
     const newRelationship = {
       primaryId,
       secondaryIds,
     };
     newRelationships.push(newRelationship);
   } catch (error) {
     console.error(
       'Failed to Convert Association to Asset Relationships:',
       error.message,
       {
         primaryAssociationId,
       },
       error.stack,
     );
   }
 }
 // Return New Relationships to Create
 return newRelationships;
};
/**
* @description Create Asset Relationships given a List of Association ID-based Relationship Records.
*  Authorization Token is generated to invoke REST API calls.
*/
const createRelationships = async () => {
 try {
   // Get API Config
   const apiConfig = Config.getApiConfig();
   // Get Credentials
   const credentials = Config.getCredentials();
   // Prepare Authentication Request
   const authenticateRequest = {
     ...apiConfig,
     ...credentials,
   };
   // Authenticate Request and Get Authorization
   const authenticateResult = await AuthenticationService.authenticate(authenticateRequest);
   const { authorization } = authenticateResult.session;
   // Get Bulk Create Relationship Records
   const { createRelationshipRecords } = RelationshipData;
   // Aggregate Association Relationships from Table to Mapping by Primary Association ID
   const associationRelationships = aggregateAssociationRelationshipRecords(createRelationshipRecords);
   // Prepare Authorized Request Context
   const requestContext = {
     ...apiConfig,
     authorization,
   };
   // Retrieve Association ID to Object ID Map
   const retrieveRequest = {
     records: createRelationshipRecords,
     associationIdNames: Config.metadata.ASSOCIATION_ID.RECORD_NAMES,
     requestContext,
   };
   const associationIdObjectIdMap = await retrieveAssetsByAssociationId(retrieveRequest);
   // Convert Association Relationships to Asset Relationships
   const convertRequest = {
     associationRelationships,
     associationIdObjectIdMap,
   };
   const newRelationships = await convertAssociationToAssetRelationships(convertRequest);
   // Loop to Create New Relationships
   for (let i = 0; i < newRelationships.length; i += 1) {
     const newRelationship = newRelationships[i];
     const { primaryId } = newRelationship;
     try {
       // Prepare Create Relationship Request
       const request = {
         ...apiConfig,
         authorization,
         ...newRelationship,
       };
       // Create Relationship
       // eslint-disable-next-line no-await-in-loop
       const result = await RelationshipService.createRelationship(request);
       // Check for Errors
       if (Array.isArray(result)) {
         for (let j = 0; j < result.length; j += 1) {
           const item = result[j];
           const secondaryId = newRelationship.secondaryIds[j];
           if (item.status === 'error') {
             console.error('Failed to Create Asset Relationship:', {
               message: item.message,
               primaryId,
               secondaryId,
             });
           }
         }
       } else {
         // Log Error Result
         console.error('Failed to Create Asset Relationship:', {
           message: result,
           primaryId,
         });
       }
     } catch (error) {
       console.error('Failed to Create Relationship:', error.message, error.stack);
     }
   }
 } catch (error) {
   console.error('Failed to Process Create Relationships:', error.message, error.stack);
 }
};
createRelationships();