import { Connection } from '@salesforce/core' ; import { AbstractGraph } from './abstractGraph' ; import { FindAllDependencies } from './DFSLib' ; import { ComponentNode , CustomField , CustomObject , Edge , FieldDefinition , MetadataComponentDependency , Node , QuickAction , ScalarNode , ValidationRule } from './NodeDefs' ; export const componentsWithParents = [ 'CustomField' , 'ValidationRule' , 'QuickAction' ] ; export class DependencyGraph extends AbstractGraph { public edges : Set < Edge > = new Set < Edge > ( ) ; private connection : Connection ; private allComponentIds : string [ ] ; private allCustomObjectIds : string [ ] ; private customFields : CustomField [ ] ; private validationRules : ValidationRule [ ] ; private customObjects : CustomObject [ ] ; private customFieldDefinitions : FieldDefinition [ ] ; private quickActions : QuickAction [ ] ; private maxNumberOfIds = 10 ; constructor ( conn ) { super ( ) ; this . connection = conn ; this . connection . bulk . pollTimeout = 25000 ; } public async init ( ) { this . allComponentIds = await this . retrieveAllComponentIds ( ) ; this . customFields = await this . retrieveCustomFields ( this . allComponentIds , new Array < CustomField > ( ) ) ; this . validationRules = await this . retrieveValidationRules ( this . allComponentIds , new Array < ValidationRule > ( ) ) ; this . quickActions = await this . retrieveQuickActions ( this . allComponentIds , new Array < QuickAction > ( ) ) ; this . allCustomObjectIds = await this . getObjectIds ( ) ; this . customObjects = await this . retrieveCustomObjects ( this . allCustomObjectIds , new Array < CustomObject > ( ) ) ; const customFieldEntities = this . customFields . map ( r => r . TableEnumOrId ) ; this . customFieldDefinitions = await this . retrieveLookupRelationships ( customFieldEntities , new Array < FieldDefinition > ( ) ) ; const lookupRelationships = this . customFieldDefinitions . filter ( x => x . DataType . startsWith ( 'Lookup' ) ) ; lookupRelationships . forEach ( element => { element . DataType = element . DataType . slice ( element . DataType . indexOf ( '(' ) + 1 , element . DataType . lastIndexOf ( ')' ) ) ; } ) ; } public buildGraph ( records ) { this . nodesMap = new Map < string , Node > ( ) ; this . edges = new Set < Edge > ( ) ; const parentRecords = this . getParentRecords ( ) ; for ( const record of records ) { let parentName = '' ; let refParentName = '' ; if ( record . RefMetadataComponentName . startsWith ( '0' ) ) { continue ; } if ( componentsWithParents . indexOf ( record . MetadataComponentType ) >= 0 ) { parentName = parentRecords . get ( record . MetadataComponentId ) + '.' ; } if ( componentsWithParents . indexOf ( record . RefMetadataComponentType ) >= 0 ) { refParentName = parentRecords . get ( record . RefMetadataComponentId ) + '.' ; } const srcId = record . MetadataComponentId ; const srcName = record . MetadataComponentName ; const srcType = record . MetadataComponentType ; const dstId = record . RefMetadataComponentId ; const dstName = record . RefMetadataComponentName ; const dstType = record . RefMetadataComponentType ; const srcDetails = new Map < string , object > ( ) ; srcDetails . set ( 'name' , ( srcName as String ) ) ; srcDetails . set ( 'type' , ( srcType as String ) ) ; srcDetails . set ( 'parent' , ( parentName as String ) ) ; const srcNode = this . getOrAddNode ( srcId , srcDetails ) ; const dstDetails = new Map < string , object > ( ) ; dstDetails . set ( 'name' , ( dstName as String ) ) ; dstDetails . set ( 'type' , ( dstType as String ) ) ; dstDetails . set ( 'parent' , ( refParentName as String ) ) ; const dstNode = this . getOrAddNode ( dstId , dstDetails ) ; this . edges . add ( { from : record . MetadataComponentId , to : record . RefMetadataComponentId } ) ; this . addEdge ( srcNode , dstNode ) ; if ( record . MetadataComponentType === 'AuraDefinition' && record . RefMetadataComponentType === 'AuraDefinitionBundle' ) { this . edges . add ( { from : record . RefMetadataComponentId , to : record . MetadataComponentId } ) ; this . addEdge ( dstNode , srcNode ) ; } } this . addFieldRelationships ( ) ; } public runDFS ( initialNodes ) { const dfs = new FindAllDependencies ( this ) ; initialNodes . forEach ( node => { const graphNode = this . getOrAddNode ( node . name , node . details ) ; dfs . runNode ( graphNode ) ; } ) ; this . nodesMap = dfs . visited ; this . edges = dfs . visitedEdges ; } public getEdges ( src ) { return ( src as ScalarNode ) . getEdges ( ) ; } public addFieldRelationships ( ) { this . customFieldDefinitions . forEach ( fielddef => { const n1 = this . getNodeShortId ( fielddef . EntityDefinitionId ) ; const objName = fielddef . DataType . slice ( fielddef . DataType . indexOf ( '(' ) + 1 , fielddef . DataType . lastIndexOf ( ')' ) ) ; const n2 = this . getNodeFromName ( objName ) ; if ( n1 != null && n2 != null ) { this . addEdge ( n1 , n2 ) ; } } ) ; } /**
    * Render as DOT format
    */ public toDotFormat ( ) { let dot = 'digraph graphname {\n' ; dot += '  rankdir=RL;\n' ; dot += '  node[shape=Mrecord, bgcolor=black, fillcolor=lightblue, style=filled];\n' ; dot += '  // Nodes\n' ; for ( const node of this . nodes ) { dot += `  X${ node . name } [label=<${ node . details . get ( 'parent' ) }${ node . details . get ( 'name' ) }<BR/><FONT POINT-SIZE="8">${ node . details . get ( 'type' ) }</FONT>>]\n` ; } dot += '  // Paths\n' ; for ( const edge of this . edges ) { dot += `  X${ edge . from }->X${ edge . to }\n` ; } dot += '}' ; return dot ; } public toJson ( ) { const jsonRepresentation = new Array < ComponentNode > ( ) ; for ( const node of this . nodes ) { const jsonNode = { id : node . name , name : ( node . details . get ( 'name' ) as String ) . valueOf ( ) , type : ( node . details . get ( 'type' ) as String ) . valueOf ( ) , parent : ( node . details . get ( 'parent' ) as String ) . valueOf ( ) } ; jsonRepresentation . push ( jsonNode ) ; } return { nodes : jsonRepresentation , edges : Array . from ( this . edges ) } ; } public getParentRecords ( ) { const parentRecords = new Map ( ) ; this . populateIdToDeveloperNameMap ( parentRecords , this . validationRules , 'EntityDefinitionId' ) ; this . populateIdToDeveloperNameMap ( parentRecords , this . customFields , 'TableEnumOrId' ) ; this . populateIdToDeveloperNameMap ( parentRecords , this . quickActions , 'SobjectType' ) ; return parentRecords ; } public async retrieveRecords < T > ( query ) { return ( await this . connection . tooling . query < T > ( query ) ) . records ; } public async retrieveBulkRecords < T > ( query ) { return ( await this . connection . bulk . query ( query ) . on ( 'record' , function ( rec ) { console . log ( rec ) ; return rec ; } ) ) . on ( 'error' , function ( err ) { console . error ( err ) ; } ) ; } private splitIds ( ids , query , maxNumberOfIds = 0 ) { const maxURIlength = 12000 ; const idNumChars = 23 ; if ( maxNumberOfIds && ids . length > maxNumberOfIds ) { var index = maxNumberOfIds ; } else if ( ( ( ids . length * idNumChars ) + query . length ) > maxURIlength ) { var index = Math . floor ( ( maxURIlength - query . length ) / idNumChars ) ; } else { var index = ids . length ; } var allIds = { left : ids . splice ( 0 , index ) , right : ids } ; return allIds ; } public async retrieveCustomFields ( ids , resultset ) { var query = `SELECT Id, TableEnumOrId FROM CustomField c WHERE c.Id In ` ; var splitIds = this . splitIds ( ids , query , this . maxNumberOfIds ) ; query = query . concat ( this . arrayToInIdString ( splitIds . left ) ) . concat ( " limit 2000" ) ; resultset = resultset . concat ( await this . retrieveRecords < CustomField > ( query ) ) ; if ( splitIds . right . length > 0 ) { this . retrieveCustomFields ( splitIds . right , resultset ) ; } return resultset ; } public async bulkRetrieveLookupRelationships ( ids , resultset ) { var query = `SELECT EntityDefinitionId,DataType,DurableId FROM FieldDefinition c WHERE c.EntityDefinitionId In ` ; var splitIds = this . splitIds ( ids , query , this . maxNumberOfIds ) ; query = query . concat ( this . arrayToInIdString ( splitIds . left ) ) . concat ( " limit 2000" ) ; resultset = resultset . concat ( await this . retrieveBulkRecords < FieldDefinition > ( query ) ) ; if ( splitIds . right . length > 0 ) { this . bulkRetrieveLookupRelationships ( splitIds . right , resultset ) ; } return resultset ; } public async retrieveLookupRelationships ( ids , resultset ) { var query = `SELECT EntityDefinitionId,DataType,DurableId FROM FieldDefinition c WHERE c.EntityDefinitionId In ` ; var splitIds = this . splitIds ( ids , query , this . maxNumberOfIds ) ; query = query . concat ( this . arrayToInIdString ( splitIds . left ) ) . concat ( " limit 2000" ) ; resultset = resultset . concat ( await this . retrieveRecords < FieldDefinition > ( query ) ) ; if ( splitIds . right . length > 0 ) { this . retrieveLookupRelationships ( splitIds . right , resultset ) ; } return resultset ; } public async retrieveValidationRules ( ids , resultset ) { var query = `SELECT Id, EntityDefinitionId FROM ValidationRule c WHERE c.Id In ` ; var splitIds = this . splitIds ( ids , query , this . maxNumberOfIds ) ; query = query . concat ( this . arrayToInIdString ( splitIds . left ) ) . concat ( " limit 2000" ) ; resultset = resultset . concat ( await this . retrieveRecords < FieldDefinition > ( query ) ) ; if ( splitIds . right . length > 0 ) { this . retrieveValidationRules ( splitIds . right , resultset ) ; } return await this . retrieveRecords < ValidationRule > ( query ) ; } public async retrieveQuickActions ( ids , resultset ) { var query = `SELECT Id, SobjectType FROM QuickActionDefinition c WHERE c.Id In ` ; var splitIds = this . splitIds ( ids , query , this . maxNumberOfIds ) ; query = query . concat ( this . arrayToInIdString ( splitIds . left ) ) . concat ( " limit 2000" ) ; resultset = resultset . concat ( await this . retrieveRecords < QuickAction > ( query ) ) ; if ( splitIds . right . length > 0 ) { this . retrieveQuickActions ( splitIds . right , resultset ) ; } return await this . retrieveRecords < QuickAction > ( query ) ; } public async retrieveCustomObjects ( ids , resultset ) { var query = `SELECT Id, DeveloperName FROM CustomObject c WHERE c.Id In ` ; var splitIds = this . splitIds ( ids , query , this . maxNumberOfIds ) ; query = query . concat ( this . arrayToInIdString ( splitIds . left ) ) . concat ( " limit 2000" ) ; resultset = resultset . concat ( await this . retrieveRecords < CustomObject > ( query ) ) ; if ( splitIds . right . length > 0 ) { this . retrieveCustomObjects ( splitIds . right , resultset ) ; } return await this . retrieveRecords < CustomObject > ( query ) ; } public getLookupRelationships ( ) { return this . customFieldDefinitions ; } private async retrieveAllComponentIds ( ) { const query = "SELECT MetadataComponentId,RefMetadataComponentId FROM MetadataComponentDependency \
    WHERE (MetadataComponentType = 'CustomField' OR RefMetadataComponentType = 'CustomField') \
    OR (MetadataComponentType = 'ValidationRule' OR RefMetadataComponentType = 'ValidationRule') \
    OR (MetadataComponentType = 'QuickAction' OR RefMetadataComponentType = 'QuickAction')" ; const customComponentIds = await this . retrieveRecords < MetadataComponentDependency > ( query ) ; const componentIds = customComponentIds . map ( r => r . MetadataComponentId ) ; const refComponentIds = customComponentIds . map ( r => r . RefMetadataComponentId ) ; let ids = componentIds . concat ( refComponentIds ) ; ids = Array . from ( new Set ( ids ) ) ; return ids ; } private getObjectIds ( ) { const fieldObjectIdRecords = this . customFields . filter ( x => x . TableEnumOrId . startsWith ( '0' ) ) ; const vruleObjectIdRecords = this . validationRules . filter ( x => x . EntityDefinitionId . startsWith ( '0' ) ) ; return [ ... fieldObjectIdRecords . map ( r => r . TableEnumOrId ) , ... vruleObjectIdRecords . map ( r => r . EntityDefinitionId ) ] ; } private populateIdToDeveloperNameMap < T > ( map , records , fieldName ) { for ( const record of records ) { let val = record [ fieldName ] ; if ( val . startsWith ( '0' ) ) { const customObject = this . customObjects . filter ( x => x . Id . startsWith ( val ) ) ; val = customObject [ 0 ] . DeveloperName + '__c' ; } map . set ( record [ 'Id' ] , val ) ; } } private arrayToInIdString ( ids ) { return `('${ ids . join ( '\',\'' ) }')` ; } }