import { ITGlueClient } from '../client'; import { QueryUtilOptions, QueryParams, RequestBody, BaseListResponse, BaseItemResponse, RelatedItemResource } from '../types'; /** * RelatedItems resource module for IT Glue API * * Provides methods to interact with the /related_items endpoint. * Related items manage relationships and connections between different resources * throughout IT Glue, enabling you to establish meaningful associations between * organizations, configurations, contacts, assets, documents, and other resources. * These relationships help create a comprehensive view of your IT infrastructure * and organizational structure. * * Relationships are essential for: * - Connecting IT assets to their responsible contacts * - Linking configurations to their hosting organizations * - Associating documents with relevant resources * - Creating dependency maps between systems * - Establishing ownership and responsibility chains * - Building comprehensive IT infrastructure views * * Supported relationship types include: * - Organizations ↔ Configurations (hosting relationships) * - Configurations ↔ Contacts (responsibility assignments) * - Assets ↔ Locations (physical placement) * - Documents ↔ Various resources (documentation links) * - Flexible Assets ↔ Other resources (custom relationships) * - Passwords ↔ Configurations (credential associations) * - Contacts ↔ Organizations (employment relationships) * * Relationship characteristics: * - Bidirectional connections (source ↔ destination) * - Metadata support (notes, descriptions, custom attributes) * - Type-specific validation and constraints * - Hierarchical and peer-to-peer relationships * - Many-to-many relationship support * - Relationship lifecycle management * * Common relationship patterns: * - Asset ownership (Contact → Configuration) * - Service dependencies (Configuration → Configuration) * - Documentation links (Document → Any resource) * - Location assignments (Asset → Location) * - Organizational structure (Contact → Organization) * - Support relationships (Contact → Multiple resources) * * ## Related Resources * Related items can connect any IT Glue resources, commonly used with: * - {@link Organizations} - Create organizational relationships and hierarchies * - {@link Configurations} - Link IT assets and establish dependencies * - {@link Contacts} - Associate people with resources and responsibilities * - {@link Documents} - Connect documentation to relevant resources * - {@link Passwords} - Associate credentials with systems and contacts * - {@link FlexibleAssets} - Create custom relationships and data connections * - {@link Locations} - Link physical sites to resources and people * - {@link Attachments} - Associate files with multiple resources * - {@link Tags} - Create tagged relationships for categorization * * @see {@link Organizations#list} for retrieving organizations to create relationships * @see {@link Configurations#list} for retrieving configurations to create relationships * @see {@link Contacts#list} for retrieving contacts to create relationships * @see {@link Documents#list} for retrieving documents to create relationships * * @example * import { ITGlueClient } from '../client'; * import { RelatedItems } from './resources/related-items'; * * const client = new ITGlueClient({ apiKey: 'your-api-key' }); * const relatedItems = new RelatedItems(client); * * // List related items for a given resource * // Note: You must filter by a source resource, e.g., configuration_id * const list = await relatedItems.list({ filter: { configuration_id: '123' } }); * * // Get a single related item * const item = await relatedItems.get('123'); * * // Create a new related item * const created = await relatedItems.create({ * data: { * type: 'related_items', * attributes: { * source_id: '123', // ID of the source resource * source_type: 'Configuration', // Type of the source resource * destination_id: '456', // ID of the destination resource * destination_type: 'Contact' // Type of the destination resource * } * } * }); * * // Update a related item * const updated = await relatedItems.update('123', { * data: { * type: 'related_items', * attributes: { * notes: 'Updated relationship notes' * } * } * }); * * // Delete a related item * await relatedItems.delete('123'); * * @category Data Management */ export declare class RelatedItems { private client; private basePath; private paginationUtil; /** * Create a RelatedItems resource instance * @param {ITGlueClient} client - ITGlueClient instance */ constructor(client: ITGlueClient); /** * List all related items for a given resource * * Retrieves relationships associated with a specific resource, showing all * connections between that resource and other items in IT Glue. This endpoint * requires filtering by a source resource to scope the relationships returned. * Use this to discover all connections for a particular item and understand * its role within your IT infrastructure. * * **Note: A filter for a source resource (e.g., configuration_id, organization_id) is required.** * This ensures efficient querying and prevents overly broad relationship listings. * * @param {QueryUtilOptions} options - Query parameters, must include a filter for a source resource * @param {boolean} [allPages=false] - If true, fetches all pages automatically * @returns {Promise>} List of related items and pagination metadata * @throws {Error} When no source filter provided (422) or access denied (403) * @example * // Basic usage - list relationships for a configuration * const results = await client.relatedItems.list({ * filter: { configuration_id: '123' }, * include: ['destination'] * }); * * console.log(`Found ${results.data.length} relationships`); * results.data.forEach(rel => { * console.log(`→ ${rel.attributes.destination_type}: ${rel.attributes.destination_id}`); * }); * * @example * // Advanced usage with pagination and sorting * const results = await client.relatedItems.list({ * filter: { organization_id: '456' }, * page: { number: 2, size: 50 }, * sort: '-created_at', // Sort by most recently created * include: ['source', 'destination'] // Include related data * }); * * @example * // Filtering relationships by destination type * const contactRelationships = await client.relatedItems.list({ * filter: { * configuration_id: '123', * destination_type: 'Contact' * }, * sort: 'destination_type' * }); * * console.log('Configuration contact relationships:'); * contactRelationships.data.forEach(rel => { * console.log(`- Contact ${rel.attributes.destination_id}: ${rel.attributes.notes || 'No notes'}`); * }); * * @example * // Comprehensive relationship discovery for a resource * async function discoverAllRelationships(resourceType, resourceId) { * const filterKey = `${resourceType.toLowerCase()}_id`; * * try { * const relationships = await client.relatedItems.list({ * filter: { [filterKey]: resourceId }, * include: ['source', 'destination'] * }); * * // Group relationships by type * const relationshipMap = {}; * relationships.data.forEach(rel => { * const destType = rel.attributes.destination_type; * if (!relationshipMap[destType]) relationshipMap[destType] = []; * relationshipMap[destType].push(rel); * }); * * console.log(`Relationships for ${resourceType} ${resourceId}:`); * Object.keys(relationshipMap).forEach(type => { * console.log(` ${type}: ${relationshipMap[type].length} connections`); * relationshipMap[type].forEach(rel => { * console.log(` → ${rel.attributes.destination_id} (${rel.attributes.notes || 'No notes'})`); * }); * }); * * return relationshipMap; * } catch (error) { * console.error('Failed to discover relationships:', error.message); * return {}; * } * } * * @example * // Get all relationships across multiple pages * const allRelationships = await client.relatedItems.list({ * filter: { organization_id: '456' } * }, true); // allPages = true * * // Analyze relationship patterns * const analysis = { * totalRelationships: allRelationships.data.length, * byDestinationType: {}, * withNotes: 0, * recentlyCreated: 0 * }; * * const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); * * allRelationships.data.forEach(rel => { * const destType = rel.attributes.destination_type; * analysis.byDestinationType[destType] = (analysis.byDestinationType[destType] || 0) + 1; * * if (rel.attributes.notes) analysis.withNotes++; * if (new Date(rel.attributes.created_at) > oneWeekAgo) analysis.recentlyCreated++; * }); * * console.log('Relationship analysis:', analysis); * * @example * // Manual pagination for large relationship datasets * async function getAllRelationships(filterParams) { * let page = 1; * let allResults = []; * let hasMore = true; * * while (hasMore) { * const response = await client.relatedItems.list({ * filter: filterParams, * page: { number: page, size: 100 }, * sort: 'created_at' * }); * * allResults = [...allResults, ...response.data]; * hasMore = response.meta.pagination.total_pages > page; * page++; * } * * return allResults; * } * * @example * // Error handling for list operations * try { * const results = await client.relatedItems.list({ * sort: 'created_at' // Missing required filter * }); * } catch (error) { * if (error.response?.status === 422) { * console.log('Source resource filter is required:', error.response.data.errors); * } else if (error.response?.status === 401) { * console.log('Authentication failed - check your API key'); * } else if (error.response?.status === 403) { * console.log('Access denied - insufficient permissions to view relationships'); * } else { * console.log('Request failed:', error.message); * } * } */ list(options: QueryUtilOptions, allPages?: boolean): Promise>; /** * Get a single related item by ID * * Retrieves detailed information about a specific relationship, including * source and destination resource details, relationship metadata, and any * associated notes or attributes. Use this to examine the specifics of a * particular connection between resources. * * @param {string} id - Related item ID (required) * @param {QueryParams} [params] - Optional query parameters * @returns {Promise>} Related item resource * @throws {Error} When relationship not found (404) or access denied (403) * @example * // Get a specific relationship * await relatedItems.get('123'); * @example * // Get relationship with source and destination details * await relatedItems.get('123', { * include: ['source', 'destination'] * }); * @example * // Error handling for relationship retrieval * try { * const relationship = await relatedItems.get('invalid-id'); * } catch (error) { * if (error.response?.status === 404) { * console.log('Relationship not found'); * } else if (error.response?.status === 403) { * console.log('Access denied to relationship'); * } * } * @example * // Examine relationship details * const relationship = await relatedItems.get('123', { * include: ['source', 'destination'] * }); * * const attrs = relationship.data.attributes; * console.log(`Relationship: ${attrs.source_type}(${attrs.source_id}) → ${attrs.destination_type}(${attrs.destination_id})`); * console.log(`Notes: ${attrs.notes || 'None'}`); * console.log(`Created: ${attrs.created_at}`); */ get(id: string, params?: QueryParams): Promise>; /** * Create a new related item * * Establishes a new relationship between two resources in IT Glue. This creates * a bidirectional connection that can be discovered from either resource. * Specify the source and destination resources along with optional metadata * to document the nature and purpose of the relationship. * * Relationships help organize your IT infrastructure by connecting related * items and establishing clear associations between different components. * * @param {RequestBody} data - Related item data, must specify source and destination * @returns {Promise>} Created related item resource * @throws {Error} When validation fails (422), resources not found (404), or access denied (403) * @example * // Create a basic relationship between configuration and contact * await relatedItems.create({ * data: { * type: 'related_items', * attributes: { * source_id: '123', * source_type: 'Configuration', * destination_id: '456', * destination_type: 'Contact', * notes: 'Primary administrator for this server' * } * } * }); * @example * // Create relationship between organization and asset * await relatedItems.create({ * data: { * type: 'related_items', * attributes: { * source_id: '789', * source_type: 'Organization', * destination_id: '101', * destination_type: 'Asset', * notes: 'Company-owned laptop assigned to employee' * } * } * }); * @example * // Error handling for relationship creation * try { * const newRelationship = await relatedItems.create({ * data: { * type: 'related_items', * attributes: { * source_id: 'invalid-id', * source_type: 'Configuration', * destination_id: '456', * destination_type: 'Contact' * } * } * }); * } catch (error) { * if (error.response?.status === 404) { * console.log('One of the resources was not found'); * } else if (error.response?.status === 422) { * console.log('Validation failed:', error.response.data.errors); * } * } * @example * // Create multiple relationships for a configuration * const configId = '123'; * const relationships = [ * { type: 'Contact', id: '456', role: 'Primary Administrator' }, * { type: 'Contact', id: '789', role: 'Backup Administrator' }, * { type: 'Organization', id: '101', role: 'Hosting Organization' } * ]; * * for (const rel of relationships) { * await relatedItems.create({ * data: { * type: 'related_items', * attributes: { * source_id: configId, * source_type: 'Configuration', * destination_id: rel.id, * destination_type: rel.type, * notes: rel.role * } * } * }); * } */ create(data: RequestBody): Promise>; /** * Update a related item by ID * * Modifies an existing relationship between resources, typically to update * metadata such as notes, descriptions, or other attributes that describe * the nature of the relationship. The source and destination resources * generally cannot be changed; create a new relationship if needed. * * @param {string} id - Related item ID (required) * @param {RequestBody} data - Updated related item data * @returns {Promise>} Updated related item resource * @throws {Error} When relationship not found (404), validation fails (422), or access denied (403) * @example * // Update relationship notes * await relatedItems.update('123', { * data: { * type: 'related_items', * attributes: { * notes: 'Updated: Now secondary administrator due to role change' * } * } * }); * @example * // Update relationship with additional metadata * await relatedItems.update('456', { * data: { * type: 'related_items', * attributes: { * notes: 'Critical dependency - requires 24/7 monitoring', * priority: 'high', * last_verified: '2024-01-15' * } * } * }); * @example * // Error handling for relationship updates * try { * const updated = await relatedItems.update('invalid-id', { * data: { * type: 'related_items', * attributes: { * notes: 'Updated notes' * } * } * }); * } catch (error) { * if (error.response?.status === 404) { * console.log('Relationship not found'); * } else if (error.response?.status === 422) { * console.log('Validation failed:', error.response.data.errors); * } * } * @example * // Update relationship with timestamp tracking * const relationship = await relatedItems.get('123'); * const currentNotes = relationship.data.attributes.notes || ''; * * await relatedItems.update('123', { * data: { * type: 'related_items', * attributes: { * notes: `${currentNotes}\n\nUpdated on ${new Date().toISOString()}: Role changed to backup administrator` * } * } * }); */ update(id: string, data: RequestBody): Promise>; /** * Delete a related item by ID * * Permanently removes a relationship between two resources. This action * cannot be undone and will break the connection between the source and * destination resources. Use this when relationships are no longer valid * or when cleaning up outdated connections. * * @param {string} id - Related item ID (required) * @returns {Promise} * @throws {Error} When relationship not found (404) or access denied (403) * @example * // Delete a relationship * await relatedItems.delete('123'); * @example * // Delete relationship with error handling * try { * await relatedItems.delete('456'); * console.log('Relationship deleted successfully'); * } catch (error) { * if (error.response?.status === 404) { * console.log('Relationship not found or already deleted'); * } else if (error.response?.status === 403) { * console.log('Insufficient permissions to delete relationship'); * } * } * @example * // Clean up relationships for a decommissioned resource * const configRelationships = await relatedItems.list({ * filter: { configuration_id: '123' } * }); * * console.log(`Removing ${configRelationships.data.length} relationships...`); * for (const relationship of configRelationships.data) { * try { * await relatedItems.delete(relationship.id); * console.log(`Deleted relationship: ${relationship.id}`); * } catch (error) { * console.log(`Failed to delete relationship ${relationship.id}:`, error.message); * } * } */ delete(id: string): Promise; /** * Bulk destroy multiple related items * * Permanently removes multiple relationships in a single operation. This is * particularly useful for cleaning up relationships when decommissioning * resources, reorganizing infrastructure, or performing bulk maintenance. * The operation is atomic - either all deletions succeed or none are applied. * * @param {RequestBody} data - Bulk destroy data containing related item IDs * @returns {Promise} Deletion confirmation * @throws {Error} When relationships not found (404), validation fails (422), or access denied (403) * @example * // Basic bulk deletion - remove multiple relationships * const deleted = await client.relatedItems.bulkDestroy({ * data: [ * { type: 'related_items', id: '123' }, * { type: 'related_items', id: '124' }, * { type: 'related_items', id: '125' } * ] * }); * * console.log('Successfully deleted multiple relationships'); * * @example * // Advanced bulk cleanup - remove all relationships for a decommissioned server * async function cleanupServerRelationships(serverId) { * try { * // Get all relationships for the server * const serverRelationships = await client.relatedItems.list({ * filter: { * source_type: 'Configuration', * source_id: serverId * } * }, true); // Get all pages * * if (serverRelationships.data.length === 0) { * console.log('No relationships found for server'); * return; * } * * console.log(`Found ${serverRelationships.data.length} relationships to remove`); * * // Prepare bulk destroy data * const bulkData = { * data: serverRelationships.data.map(rel => ({ * type: 'related_items', * id: rel.id * })) * }; * * // Execute bulk destroy * await client.relatedItems.bulkDestroy(bulkData); * console.log(`Successfully removed all ${serverRelationships.data.length} relationships`); * * } catch (error) { * console.error('Failed to cleanup server relationships:', error.message); * throw error; * } * } * * @example * // Conditional bulk deletion - remove only specific types of relationships * async function removeObsoleteRelationships(organizationId) { * try { * // Get all relationships for the organization * const allRelationships = await client.relatedItems.list({ * filter: { * source_type: 'Organization', * source_id: organizationId * } * }, true); * * // Filter for obsolete relationship types * const obsoleteTypes = ['OldAssetType', 'DeprecatedConfiguration', 'ArchivedContact']; * const obsoleteRelationships = allRelationships.data.filter(rel => * obsoleteTypes.includes(rel.attributes.destination_type) * ); * * if (obsoleteRelationships.length === 0) { * console.log('No obsolete relationships found'); * return; * } * * console.log(`Removing ${obsoleteRelationships.length} obsolete relationships`); * * // Batch into smaller groups to avoid API limits * const batchSize = 50; * const batches = []; * * for (let i = 0; i < obsoleteRelationships.length; i += batchSize) { * batches.push(obsoleteRelationships.slice(i, i + batchSize)); * } * * let totalDeleted = 0; * for (const batch of batches) { * const batchData = { * data: batch.map(rel => ({ * type: 'related_items', * id: rel.id * })) * }; * * await client.relatedItems.bulkDestroy(batchData); * totalDeleted += batch.length; * console.log(`Deleted batch of ${batch.length} relationships (${totalDeleted}/${obsoleteRelationships.length})`); * } * * console.log(`Successfully removed all ${totalDeleted} obsolete relationships`); * * } catch (error) { * console.error('Failed to remove obsolete relationships:', error.message); * throw error; * } * } * * @example * // Bulk deletion with comprehensive error handling and recovery * async function robustBulkDestroy(relationshipIds) { * const maxRetries = 3; * let attempt = 0; * * while (attempt < maxRetries) { * try { * const bulkData = { * data: relationshipIds.map(id => ({ * type: 'related_items', * id: id * })) * }; * * await client.relatedItems.bulkDestroy(bulkData); * console.log(`Bulk destroy successful on attempt ${attempt + 1}`); * return; * * } catch (error) { * attempt++; * * if (error.response?.status === 404) { * console.log('Some relationships not found - checking individual existence'); * * // Filter out non-existent relationships and retry * const validIds = []; * for (const id of relationshipIds) { * try { * await client.relatedItems.get(id); * validIds.push(id); * } catch (getError) { * if (getError.response?.status === 404) { * console.log(`Relationship ${id} not found, skipping`); * } else { * validIds.push(id); // Keep it if error is not 404 * } * } * } * * if (validIds.length > 0) { * relationshipIds = validIds; // Update the list for retry * console.log(`Retrying with ${validIds.length} valid relationships`); * continue; * } else { * console.log('No valid relationships found for bulk destroy'); * return; * } * * } else if (error.response?.status === 422) { * console.log('Validation errors detected:'); * if (error.response.data.errors) { * error.response.data.errors.forEach((err, index) => { * console.log(`- Relationship ${index + 1}: ${err.detail}`); * }); * } * * // Don't retry validation errors * throw new Error('Bulk destroy failed due to validation errors'); * * } else if (error.response?.status === 409) { * console.log(`Conflict detected on attempt ${attempt}, retrying in ${attempt * 1000}ms...`); * await new Promise(resolve => setTimeout(resolve, attempt * 1000)); * continue; * * } else if (attempt === maxRetries) { * console.log(`Bulk destroy failed after ${maxRetries} attempts`); * throw error; * } else { * console.log(`Attempt ${attempt} failed, retrying...`); * await new Promise(resolve => setTimeout(resolve, 1000)); * } * } * } * } * * @example * // Real-world scenario: Organization merger cleanup * async function mergeOrganizationRelationships(sourceOrgId, targetOrgId) { * try { * console.log(`Merging relationships from organization ${sourceOrgId} to ${targetOrgId}`); * * // Get all relationships for the source organization * const sourceRelationships = await client.relatedItems.list({ * filter: { * source_type: 'Organization', * source_id: sourceOrgId * } * }, true); * * if (sourceRelationships.data.length === 0) { * console.log('No relationships found for source organization'); * return; * } * * console.log(`Found ${sourceRelationships.data.length} relationships to migrate`); * * // Create new relationships for target organization * const migrationResults = []; * for (const rel of sourceRelationships.data) { * try { * const newRelationship = await client.relatedItems.create({ * data: { * type: 'related_items', * attributes: { * source_id: targetOrgId, * source_type: 'Organization', * destination_id: rel.attributes.destination_id, * destination_type: rel.attributes.destination_type, * notes: `Migrated from org ${sourceOrgId}: ${rel.attributes.notes || ''}` * } * } * }); * * migrationResults.push({ * oldId: rel.id, * newId: newRelationship.data.id, * status: 'migrated' * }); * * } catch (createError) { * migrationResults.push({ * oldId: rel.id, * status: 'failed', * error: createError.message * }); * } * } * * // Remove old relationships (only successful migrations) * const successfulMigrations = migrationResults.filter(r => r.status === 'migrated'); * if (successfulMigrations.length > 0) { * const bulkData = { * data: successfulMigrations.map(r => ({ * type: 'related_items', * id: r.oldId * })) * }; * * await client.relatedItems.bulkDestroy(bulkData); * console.log(`Successfully migrated and cleaned up ${successfulMigrations.length} relationships`); * } * * // Report results * const failed = migrationResults.filter(r => r.status === 'failed'); * if (failed.length > 0) { * console.log(`${failed.length} relationships failed to migrate:`); * failed.forEach(f => console.log(`- ${f.oldId}: ${f.error}`)); * } * * return migrationResults; * * } catch (error) { * console.error('Organization merger failed:', error.message); * throw error; * } * } * * @example * // Bulk deletion with progress tracking for large datasets * async function bulkDestroyWithProgress(relationshipIds) { * const batchSize = 25; // Smaller batches for better progress tracking * const totalBatches = Math.ceil(relationshipIds.length / batchSize); * let completedBatches = 0; * let totalDeleted = 0; * * console.log(`Starting bulk destroy of ${relationshipIds.length} relationships in ${totalBatches} batches`); * * for (let i = 0; i < relationshipIds.length; i += batchSize) { * const batch = relationshipIds.slice(i, i + batchSize); * const batchNumber = Math.floor(i / batchSize) + 1; * * try { * const bulkData = { * data: batch.map(id => ({ * type: 'related_items', * id: id * })) * }; * * await client.relatedItems.bulkDestroy(bulkData); * totalDeleted += batch.length; * completedBatches++; * * const progress = Math.round((completedBatches / totalBatches) * 100); * console.log(`Batch ${batchNumber}/${totalBatches} completed (${progress}%) - Deleted ${totalDeleted}/${relationshipIds.length} relationships`); * * // Small delay to avoid overwhelming the API * if (batchNumber < totalBatches) { * await new Promise(resolve => setTimeout(resolve, 100)); * } * * } catch (error) { * console.error(`Batch ${batchNumber} failed:`, error.message); * * // Continue with remaining batches * if (error.response?.status !== 422) { * console.log('Continuing with remaining batches...'); * continue; * } else { * throw error; // Stop on validation errors * } * } * } * * console.log(`Bulk destroy completed: ${totalDeleted}/${relationshipIds.length} relationships deleted`); * return { totalDeleted, totalRequested: relationshipIds.length }; * } */ bulkDestroy(data: RequestBody): Promise; }