## 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`** ```javascript const getApiConfig = () => { const endpointUrl = ''; const apiKey = ''; const config = { endpointUrl, piKey, }; return config; }; const getCredentials = () => { const clientId = ''; const username = ''; const password = ''; const credentials = { clientId, username, password, }; return credentials; }; const metadata = { ASSOCIATION_ID: { RECORD_NAMES: [ 'primaryAssociationId', 'secondaryAssociationId', ], ATTRIBUTE_NAME: '', }, }; 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`** ```javascript 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`** ```javascript 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(); ```