├── .gitignore ├── CI ├── build.xml └── salesforce_ant_31.0 │ └── ant-salesforce.jar ├── README.md ├── assets ├── DefaultDiagram.pdf └── DefaultDiagram.png └── src ├── classes ├── ERDDisplayType.cls ├── ERDDisplayType.cls-meta.xml ├── ERDEntity.cls ├── ERDEntity.cls-meta.xml ├── ERDEntityField.cls ├── ERDEntityField.cls-meta.xml ├── ERDEntityGroup.cls ├── ERDEntityGroup.cls-meta.xml ├── ERDEntityRelationship.cls ├── ERDEntityRelationship.cls-meta.xml ├── ERDException.cls ├── ERDException.cls-meta.xml ├── ERDGenerator.cls ├── ERDGenerator.cls-meta.xml ├── ERDGeneratorController.cls ├── ERDGeneratorController.cls-meta.xml ├── ERDGeneratorControllerTest.cls ├── ERDGeneratorControllerTest.cls-meta.xml ├── ERDGeneratorTest.cls ├── ERDGeneratorTest.cls-meta.xml ├── ERDInspector.cls ├── ERDInspector.cls-meta.xml ├── ERDListUtils.cls ├── ERDListUtils.cls-meta.xml ├── ERDPersister.cls ├── ERDPersister.cls-meta.xml ├── ERDPersisterTests.cls ├── ERDPersisterTests.cls-meta.xml ├── ERDSettingProcessor.cls ├── ERDSettingProcessor.cls-meta.xml ├── ERDSettingsV1.cls ├── ERDSettingsV1.cls-meta.xml ├── ERDSettingsV2.cls ├── ERDSettingsV2.cls-meta.xml ├── ERDSettingsV3.cls ├── ERDSettingsV3.cls-meta.xml ├── ERDTemplateEngine.cls ├── ERDTemplateEngine.cls-meta.xml ├── ERDTemplateEngineTests.cls ├── ERDTemplateEngineTests.cls-meta.xml ├── ERDTemplateType.cls ├── ERDTemplateType.cls-meta.xml ├── ERDUtils.cls └── ERDUtils.cls-meta.xml ├── components ├── JQuerySelectable.component └── JQuerySelectable.component-meta.xml ├── objects └── ERD__c.object ├── package.xml ├── pages ├── ERDDownloader.page ├── ERDDownloader.page-meta.xml ├── ERDGeneratorPage.page └── ERDGeneratorPage.page-meta.xml ├── staticresources ├── DefaultDiagram.resource ├── DefaultDiagram.resource-meta.xml ├── DefaultTemplate.resource ├── DefaultTemplate.resource-meta.xml ├── ERDGeneratorJS.resource ├── ERDGeneratorJS.resource-meta.xml ├── VizJS.resource ├── VizJS.resource-meta.xml ├── WorkingImage.resource └── WorkingImage.resource-meta.xml └── tabs └── ERD_Generator.tab /.gitignore: -------------------------------------------------------------------------------- 1 | Referenced Packages 2 | .project 3 | salesforce.schema 4 | .settings/com.salesforce.ide.core.prefs 5 | config 6 | ForceERD.sublime-project 7 | ForceERD.sublime-settings 8 | ForceERD.sublime-workspace 9 | apex-scripts 10 | CI/salesforce_ant_31.0/sample 11 | Readme.html 12 | debug 13 | .README.md.html 14 | -------------------------------------------------------------------------------- /CI/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CI/salesforce_ant_31.0/ant-salesforce.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevebuik/GraphVizForce/95095cc8d768d57838df22fb89967948f48427f1/CI/salesforce_ant_31.0/ant-salesforce.jar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Full documentation can be found here](http://stevebuik.github.io/GraphVizForce) 2 | 3 | This project contains the source for developers. Please follow the link above for a description and instructions for getting started. -------------------------------------------------------------------------------- /assets/DefaultDiagram.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevebuik/GraphVizForce/95095cc8d768d57838df22fb89967948f48427f1/assets/DefaultDiagram.pdf -------------------------------------------------------------------------------- /assets/DefaultDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevebuik/GraphVizForce/95095cc8d768d57838df22fb89967948f48427f1/assets/DefaultDiagram.png -------------------------------------------------------------------------------- /src/classes/ERDDisplayType.cls: -------------------------------------------------------------------------------- 1 | public with sharing class ERDDisplayType { 2 | 3 | public static final String LABEL_ANYTYPE = 'anytype'; 4 | public static final String LABEL_BASE64 = 'base64'; 5 | public static final String LABEL_BOOLEAN = 'Boolean'; 6 | public static final String LABEL_COMBOBOX = 'Combobox'; 7 | public static final String LABEL_CURRENCY = 'Currency'; 8 | public static final String LABEL_DATA_CATEGORY_GROUP_REFERENCE = 'DataCategoryGroupReference'; 9 | public static final String LABEL_DATE = 'Date'; 10 | public static final String LABEL_DATETIME = 'DateTime'; 11 | public static final String LABEL_DOUBLE = 'Double'; 12 | public static final String LABEL_EMAIL = 'Email'; 13 | public static final String LABEL_ENCRYPTEDSTRING = 'EncryptedString'; 14 | public static final String LABEL_ID = 'ID'; 15 | public static final String LABEL_INTEGER = 'Integer'; 16 | public static final String LABEL_MULTIPICKLIST = 'MultiPicklist'; 17 | public static final String LABEL_PERCENT = 'Percent'; 18 | public static final String LABEL_PHONE = 'Phone'; 19 | public static final String LABEL_PICKLIST = 'Picklist'; 20 | public static final String LABEL_REFERENCE = 'Reference'; 21 | public static final String LABEL_STRING = 'String'; 22 | public static final String LABEL_TEXTAREA = 'TextArea'; 23 | public static final String LABEL_TIME = 'Time'; 24 | public static final String LABEL_URL = 'URL'; 25 | 26 | public static String getDisplayTypeLabel(Schema.DisplayType displayTypeEnum) 27 | { 28 | if(displayTypeEnum == Schema.DisplayType.anytype) {return LABEL_ANYTYPE;} 29 | if(displayTypeEnum == Schema.DisplayType.base64) {return LABEL_BASE64;} 30 | if(displayTypeEnum == Schema.DisplayType.Boolean) {return LABEL_BOOLEAN;} 31 | if(displayTypeEnum == Schema.DisplayType.Combobox) {return LABEL_COMBOBOX;} 32 | if(displayTypeEnum == Schema.DisplayType.Currency) {return LABEL_CURRENCY;} 33 | if(displayTypeEnum == Schema.DisplayType.DataCategoryGroupReference) {return LABEL_DATA_CATEGORY_GROUP_REFERENCE;} 34 | if(displayTypeEnum == Schema.DisplayType.Date) {return LABEL_DATE;} 35 | if(displayTypeEnum == Schema.DisplayType.DateTime) {return LABEL_DATETIME;} 36 | if(displayTypeEnum == Schema.DisplayType.Double) {return LABEL_DOUBLE;} 37 | if(displayTypeEnum == Schema.DisplayType.Email) {return LABEL_EMAIL;} 38 | if(displayTypeEnum == Schema.DisplayType.EncryptedString) {return LABEL_ENCRYPTEDSTRING;} 39 | if(displayTypeEnum == Schema.DisplayType.ID) {return LABEL_ID;} 40 | if(displayTypeEnum == Schema.DisplayType.Integer) {return LABEL_INTEGER;} 41 | if(displayTypeEnum == Schema.DisplayType.Percent) {return LABEL_PERCENT;} 42 | if(displayTypeEnum == Schema.DisplayType.Phone) {return LABEL_PHONE;} 43 | if(displayTypeEnum == Schema.DisplayType.Picklist) {return LABEL_PICKLIST;} 44 | if(displayTypeEnum == Schema.DisplayType.Reference) {return LABEL_REFERENCE;} 45 | if(displayTypeEnum == Schema.DisplayType.String) {return LABEL_STRING;} 46 | if(displayTypeEnum == Schema.DisplayType.TextArea) {return LABEL_TEXTAREA;} 47 | if(displayTypeEnum == Schema.DisplayType.Time) {return LABEL_TIME;} 48 | if(displayTypeEnum == Schema.DisplayType.URL) {return LABEL_URL;} 49 | 50 | return ''; 51 | } 52 | } -------------------------------------------------------------------------------- /src/classes/ERDDisplayType.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDEntity.cls: -------------------------------------------------------------------------------- 1 | /* 2 | Name: ERDEntity 3 | Purpose: 4 | This class is the data model for ERD 5 | */ 6 | public with sharing class ERDEntity 7 | { 8 | public String objectName {get;set;} 9 | public Boolean isCustom {get;set;} 10 | public List fieldList {get;set;} 11 | } -------------------------------------------------------------------------------- /src/classes/ERDEntity.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDEntityField.cls: -------------------------------------------------------------------------------- 1 | public with sharing class ERDEntityField { 2 | public String fieldName{get;set;} 3 | public String fieldType{get;set;} 4 | 5 | public ERDEntityField(String fName, String fType) 6 | { 7 | this.fieldName = fName; 8 | this.fieldType = fType; 9 | } 10 | } -------------------------------------------------------------------------------- /src/classes/ERDEntityField.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDEntityGroup.cls: -------------------------------------------------------------------------------- 1 | public with sharing class ERDEntityGroup { 2 | public String groupName {get;set;} 3 | public Boolean selected {get;set;} 4 | 5 | public ERDEntityGroup(String grpName, Boolean isSelected) 6 | { 7 | this.groupName = grpName; 8 | this.selected = isSelected; 9 | } 10 | } -------------------------------------------------------------------------------- /src/classes/ERDEntityGroup.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDEntityRelationship.cls: -------------------------------------------------------------------------------- 1 | /* 2 | Name: ERDEntityRelationship 3 | Purpose: 4 | This class is the data model for ERD relationship 5 | */ 6 | public with sharing class ERDEntityRelationship 7 | { 8 | public String lookupFieldName {get;set;} 9 | public String parentObjectName {get;set;} 10 | public String childObjectName {get;set;} 11 | public Boolean isCascadeDelete {get;set;} // Reflects Master Detail Relationship 12 | } -------------------------------------------------------------------------------- /src/classes/ERDEntityRelationship.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDException.cls: -------------------------------------------------------------------------------- 1 | public with sharing class ERDException extends Exception { 2 | 3 | } -------------------------------------------------------------------------------- /src/classes/ERDException.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDGenerator.cls: -------------------------------------------------------------------------------- 1 | /* 2 | Name: ERDGenerator 3 | Purpose: generate an ERD that Graphviz can understand 4 | */ 5 | 6 | public with sharing class ERDGenerator 7 | { 8 | private Integer describeChildCount = 0; 9 | 10 | private ERDTemplateEngine templateEngine; 11 | 12 | public ERDGenerator(String resourceName) { 13 | this(new ERDTemplateEngine.StaticResourceTemplateSource(resourceName)); 14 | } 15 | 16 | public ERDGenerator(ERDTemplateEngine.TemplateSource source) { 17 | templateEngine = new ERDTemplateEngine(source); 18 | } 19 | 20 | public String generateERD(ERDSettingsV3 settings) 21 | { 22 | ERDInspector inspector = new ERDInspector(); 23 | ERDSettingProcessor processor = new ERDSettingProcessor(); 24 | Boolean includeFields = settings.includeFields; 25 | Boolean includeStandardUserLookup = settings.includeStandardUserLookup; 26 | Map>> erdMap = new Map>>(); 27 | Set allSelectedObjects = processor.retrieveAllObjectsForGroups(settings); 28 | List allObjectRelationshipList = new List(); 29 | Map> groupToERDEntityListMap = new Map>(); 30 | 31 | for(String grp : settings.groupToObjectsToFieldsMap.keySet()) 32 | { 33 | if(settings.includedGroups.contains(grp)) 34 | { 35 | erdMap.put(grp,settings.groupToObjectsToFieldsMap.get(grp)); 36 | } 37 | } 38 | 39 | inspector.inspectERDSchema(allSelectedObjects, 40 | erdMap, 41 | groupToERDEntityListMap, 42 | allObjectRelationshipList 43 | ); 44 | 45 | String subgraphContent = generateSubgraphContent(groupToERDEntityListMap, includeFields); 46 | String crossGroupRelationshipContent = generateCrossGroupRelationshipContent(allObjectRelationshipList, includeStandardUserLookup); 47 | 48 | return renderERDGraphvizContent(subgraphContent, crossGroupRelationshipContent); 49 | } 50 | 51 | private static final List COLORS = new List { 52 | 'powderblue','olivedrab3','peachpuff','orange','lightsalmon4','indianred2' 53 | ,'mediumpurple','tomato2','orchid','wheat','palegreen','darkslateblue' 54 | }; 55 | 56 | private String generateSubgraphContent(Map> groupToERDEntityListMap, 57 | Boolean includeFields) { 58 | String subgraphsContent = ''; 59 | Integer subgraphIndex = 0; 60 | 61 | Set groupNameSet = groupToERDEntityListMap.keySet(); 62 | 63 | // For each group of objects specified by user in UI 64 | for(String groupName : groupNameSet){ 65 | // Prepare List of entities, List of relationships 66 | List entities = groupToERDEntityListMap.get(groupName); 67 | 68 | // Generate the subgraph section with group index, groupname, object names for the group, entity List and relationship List 69 | String groupSubGraph = generateSubgraph( 70 | subgraphIndex, 71 | groupName, 72 | COLORS.get(subgraphIndex), 73 | entities, 74 | includeFields); 75 | subgraphsContent += groupSubGraph; 76 | subgraphIndex++; 77 | } 78 | 79 | return subgraphsContent; 80 | } 81 | 82 | private String generateCrossGroupRelationshipContent(List allObjectRelationshipList, Boolean includeStandardUserLookup) 83 | { 84 | String crossGroupRelationshipContent = ''; 85 | 86 | if(!allObjectRelationshipList.isEmpty()) 87 | { 88 | for(ERDEntityRelationship crossRelationship : allObjectRelationshipList){ 89 | 90 | if(includeStandardUserLookup || !ERDUtils.isStandardUserLookupRelationship(crossRelationship)) 91 | { 92 | ERDTemplateType templateType = (crossRelationship.isCascadeDelete?ERDTemplateType.MD_Relationship:ERDTemplateType.LOOKUP_RELATIONSHIP); 93 | crossGroupRelationshipContent += templateEngine.render(templateType, 94 | new Map{ 95 | 'from' => crossRelationship.parentObjectName, 96 | 'to' => crossRelationship.childObjectName 97 | }); 98 | } 99 | } 100 | } 101 | 102 | return crossGroupRelationshipContent; 103 | } 104 | 105 | private String renderERDGraphvizContent(String subgraphContent, String crossGroupRelationshipContent) 106 | { 107 | // Render ERD graphviz content using template engine 108 | return templateEngine.render(ERDTemplateType.MAIN, 109 | new Map{ 110 | 'content' => subgraphContent + crossGroupRelationshipContent 111 | }); 112 | } 113 | 114 | private String generateFields(List entityFields) 115 | { 116 | String entityFieldsOutput = ''; 117 | 118 | for(ERDEntityField entityField : entityFields) 119 | { 120 | System.debug(LoggingLevel.ERROR, '@@@@@@@@@@@@@@@entityField.fieldType:' + entityField.fieldType); 121 | entityFieldsOutput += templateEngine.render(ERDTemplateType.FIELD, 122 | new Map{ 123 | 'name' => entityField.fieldName, 124 | 'fieldType' => entityField.fieldType 125 | }); 126 | } 127 | 128 | return entityFieldsOutput; 129 | } 130 | 131 | private String generateSubgraph( 132 | Integer subgraphIndex, 133 | String groupName, 134 | String colour, 135 | List entities, 136 | Boolean includeFields 137 | ){ 138 | 139 | String entityOutput = ''; 140 | String entityRelationshipOutput = ''; 141 | String entityFieldsOutput = ''; 142 | 143 | for(ERDEntity entity : entities){ 144 | 145 | if(includeFields) { 146 | entityFieldsOutput = generateFields(entity.fieldList); 147 | } else { 148 | entityFieldsOutput = ''; 149 | } 150 | 151 | entityOutput += templateEngine.render(ERDTemplateType.ENTITY, 152 | new Map{ 153 | 'name' => entity.objectName, 154 | 'fields' => entityFieldsOutput 155 | }); 156 | } 157 | 158 | return templateEngine.render(ERDTemplateType.CLUSTER, 159 | new Map{ 160 | 'sequence' => ''+subgraphIndex, 161 | 'color' => colour, 162 | 'name' => groupName, 163 | 'content' => entityOutput + entityRelationshipOutput 164 | }); 165 | 166 | } 167 | 168 | } -------------------------------------------------------------------------------- /src/classes/ERDGenerator.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDGeneratorController.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description ERDGeneratorController 3 | * This is the controller for visualforce page ERDGeneratorPage 4 | * @date 21/09/2014 5 | */ 6 | 7 | public class ERDGeneratorController 8 | { 9 | public static final String EXTERNAL_TEMPLATE = 'CalloutTemplate'; 10 | public static final String GROUP_STANDARD = 'Standard'; 11 | public static final String GROUP_CUSTOM = 'Custom'; 12 | public static final String DEFAULT_DIAGRAM_NAME = 'Default Diagram'; 13 | 14 | public List erdGroups{get;set;} 15 | public String newGroupName{get;set;} 16 | public String selectedGroup{get;set;} 17 | public String groupToDelete{get;set;} 18 | 19 | public String newSettingName{get;set;} 20 | public String selectedSetting{get;set;} 21 | public String generatedContent{get;set;} 22 | 23 | public String template{get;set;} 24 | public String fileURL{get;set;} 25 | 26 | public String selectedSingleObject{get;set;} 27 | 28 | @TestVisible private ERDPersister persister; 29 | @TestVisible private ERDSettingProcessor processor; 30 | @TestVisible private ERDInspector inspector; 31 | private List settingNamesList; 32 | 33 | public String currentSetting{ 34 | get { 35 | if (currentSetting == null) { 36 | currentSetting = ''; 37 | } 38 | return currentSetting; 39 | } 40 | set; 41 | } 42 | 43 | public List allObjectNames { 44 | get { 45 | if(allObjectNames == null) { 46 | allObjectNames = ERDUtils.getAllObjectNames(); 47 | } 48 | return allObjectNames; 49 | } 50 | set; 51 | } 52 | 53 | @TestVisible 54 | private ERDSettingsV3 settings{ 55 | get { 56 | if (settings == null) settings = new ERDSettingsV3(); 57 | return settings; 58 | } 59 | set; 60 | } 61 | 62 | public List settingSelectOptions 63 | { 64 | get 65 | { 66 | List options = new List(); 67 | 68 | if(settingNamesList != null && !settingNamesList.isEmpty()) 69 | { 70 | settingNamesList.sort(); 71 | for(String setting : settingNamesList) 72 | { 73 | SelectOption option = new SelectOption(setting,setting); 74 | options.add(option); 75 | } 76 | } 77 | 78 | return options; 79 | } 80 | } 81 | 82 | public ERDGeneratorController() { 83 | System.debug(LoggingLevel.ERROR, '@@@@@@ERDGeneratorController constructor is called'); 84 | processor = new ERDSettingProcessor(); 85 | inspector = new ERDInspector(); 86 | initERDPersister(); 87 | } 88 | 89 | /***********Initialisation************/ 90 | private void initERDPersister() { 91 | 92 | persister = new ERDPersister(); 93 | 94 | // Load first setting if setting List is not empty 95 | settingNamesList = persister.getSettingsNameList(); 96 | } 97 | 98 | // if no diagrams exist i.e. new user, then seed one from a static resource 99 | public PageReference ensureDefaultDiagram() { 100 | System.debug(LoggingLevel.ERROR,'@@@@@ensureDefaultDiagram is called'); 101 | if (settingNamesList.size() == 0) { 102 | System.debug(LoggingLevel.ERROR,'@@@@@settingNamesList is empty'); 103 | selectedSetting = DEFAULT_DIAGRAM_NAME; 104 | currentSetting = selectedSetting; 105 | settings = (ERDSettingsV3) persister.getDefaultSettings(); 106 | persister.saveSettings(selectedSetting, settings); 107 | initUI(); 108 | PageReference ref = Page.ERDGeneratorPage; 109 | ref.setRedirect(true); 110 | return ref; 111 | } else { 112 | System.debug(LoggingLevel.ERROR,'@@@@@settingNamesList is not empty'); 113 | settingNamesList.sort(); 114 | selectedSetting = settingNamesList[0]; 115 | currentSetting = selectedSetting; 116 | loadSelectedSetting(); 117 | initUI(); 118 | return null; 119 | } 120 | } 121 | 122 | private void initUI() 123 | { 124 | // Get ERD Entity Groups 125 | erdGroups = ERDUtils.getERDEntityGroups(settings.groupToObjectsToFieldsMap.keySet(),settings.includedGroups); 126 | 127 | // Reset UI persistency 128 | selectedGroup = null; 129 | selectedSingleObject = null; 130 | } 131 | 132 | /********Getter Setter Methods********/ 133 | public Boolean getIncludeFields() { 134 | return settings.includeFields; 135 | } 136 | 137 | public void setIncludeFields(Boolean include) { 138 | settings.includeFields = include; 139 | } 140 | 141 | public Boolean getIncludeStandardUserLookup(){ 142 | return settings.includeStandardUserLookup; 143 | } 144 | 145 | public void setIncludeStandardUserLookup(Boolean include){ 146 | settings.includeStandardUserLookup = include; 147 | } 148 | 149 | public List getAllSelectedObjects() 150 | { 151 | List sorted = new List(); 152 | if(settings != null) {sorted = new List(processor.retrieveAllObjectsForGroups(settings));} 153 | sorted.sort(); 154 | return sorted; 155 | } 156 | 157 | public String getObjectsForSelectedGroup() 158 | { 159 | if(selectedGroup == null || String.isBlank(selectedGroup)) return ''; 160 | return getCSV(inspector.getAvailableObjectsForGroup(selectedGroup,allObjectNames,settings.groupToObjectsToFieldsMap)); 161 | } 162 | 163 | public String getSelectedObjects() { 164 | if (settings == null || selectedGroup == null || settings.groupToObjectsToFieldsMap.get(selectedGroup) == null) return null; 165 | return getCSV(new List(settings.groupToObjectsToFieldsMap.get(selectedGroup).keySet())); 166 | } 167 | 168 | public void setSelectedObjects(String csv) { 169 | 170 | if (settings == null || selectedGroup == null) return; 171 | processor.setSelectedObjectsToGroup(settings, csv, selectedGroup); 172 | } 173 | 174 | public String getAllFieldsForSelectedObject() 175 | { 176 | if(selectedSingleObject == null || String.isBlank(selectedSingleObject)) return ''; 177 | List fieldNames = new List(); 178 | fieldNames = inspector.getFieldNamesByObjectName(selectedSingleObject); 179 | return getCSV(fieldNames); 180 | } 181 | 182 | public String getSelectedObjectFields() 183 | { 184 | if(settings == null || selectedSingleObject == null || String.isBlank(selectedSingleObject)) return null; 185 | return getCSV(processor.retrieveFieldsForObject(settings, selectedSingleObject)); 186 | } 187 | 188 | /** 189 | * @description This is the problematic setter: 190 | * Problem: When select object in selectlist, selctedsingleobject is updated immediately when this setter is called, csv passed into this method is still the old set of fields, that gets updated into the new selected single object 191 | * Solution: in UI, use table with edit button instead of selectlist 192 | */ 193 | public void setSelectedObjectFields(String csv) 194 | { 195 | if (settings == null || selectedSingleObject == null || String.isBlank(selectedSingleObject)) return; 196 | processor.setSelectedFieldsToObject(settings, csv, selectedSingleObject); 197 | } 198 | 199 | 200 | /********UI Interactive Methods********/ 201 | public void loadSelectedSetting() 202 | { 203 | if(String.isBlank(selectedSetting)) 204 | { 205 | ApexPages.addmessage(new ApexPages.message(ApexPages.severity.WARNING,'Please select a setting')); 206 | return; 207 | } 208 | 209 | // Update currentSetting 210 | currentSetting = selectedSetting; 211 | settings = (ERDSettingsV3) persister.getSettings(selectedSetting); 212 | 213 | initUI(); 214 | } 215 | 216 | public PageReference refreshObjectFieldSelection() 217 | { 218 | System.debug(LoggingLevel.ERROR, '@@@@@@refreshObjectFieldSelection is called'); 219 | updateERDGroupSelection(); 220 | selectedSingleObject = null; 221 | return null; 222 | } 223 | 224 | public void updateERDGroupSelection() 225 | { 226 | processor.setERDGroupSelection(settings,erdGroups); 227 | } 228 | 229 | public void addNewGroupWithInput() { 230 | if (newGroupName == null || erdGroups == null) {return;} 231 | 232 | ERDEntityGroup newGroup = new ERDEntityGroup(newGroupName,true); 233 | erdGroups.add(newGroup); 234 | 235 | processor.addNewERDGroup(settings,newGroupName); 236 | } 237 | 238 | public void deleteERDGroup() { 239 | if(erdGroups != null) 240 | { 241 | for(Integer i=0;i getTemplates() { 344 | List templates = new List(); 345 | for (StaticResource sr : [Select s.Name From StaticResource s 346 | where s.Description like '#graphviztemplate%']) { 347 | templates.add(new SelectOption(sr.Name,sr.Name)); 348 | } 349 | templates.add(new SelectOption(EXTERNAL_TEMPLATE, '')); 350 | return templates; 351 | } 352 | 353 | 354 | /********Local Methods********/ 355 | // JQuery Selectable / VF Hidden Input Interop 356 | private String getCSV(List options) { 357 | System.debug(LoggingLevel.ERROR, '@@@@getCSV:' + options); 358 | if (options == null) return ''; 359 | String result = ''; 360 | for (String o : options) { 361 | if (result.length() > 0) { 362 | result += ','; 363 | } 364 | result += o; 365 | } 366 | return result; 367 | } 368 | 369 | } -------------------------------------------------------------------------------- /src/classes/ERDGeneratorController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDGeneratorControllerTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class ERDGeneratorControllerTest 3 | { 4 | 5 | @isTest 6 | static void testSettingsWithDefaultDiagram() 7 | { 8 | // Initiate page reference 9 | PageReference pageRef = Page.ERDGeneratorPage; 10 | Test.setCurrentPage(pageRef); 11 | System.debug(LoggingLevel.ERROR, '@@@@@@PageReference is initialised'); 12 | 13 | Test.startTest(); 14 | 15 | /* Test controller without saved diagram */ 16 | // Instantiate a new controller with all parameters in the page 17 | ERDGeneratorController controller; 18 | controller = new ERDGeneratorController(); 19 | System.debug(LoggingLevel.ERROR, '@@@@@@ERDGeneratorController is initialised'); 20 | controller.ensureDefaultDiagram(); 21 | 22 | // Test d1 document exists 23 | List docs = [SELECT Id, Body FROM Document WHERE Name = :ERDGeneratorController.DEFAULT_DIAGRAM_NAME AND FolderId = :controller.persister.folderId]; 24 | System.assertEquals(1, docs.size()); 25 | 26 | // Test loading setting d1 27 | System.assertEquals(ERDGeneratorController.DEFAULT_DIAGRAM_NAME, controller.selectedSetting); 28 | System.assertEquals(ERDGeneratorController.DEFAULT_DIAGRAM_NAME, controller.currentSetting); 29 | 30 | /* Test controller with saved diagram */ 31 | // Load page with saved diagram "Standard Objects" 32 | controller = new ERDGeneratorController(); 33 | controller.ensureDefaultDiagram(); 34 | 35 | // Test controller UI methods 36 | System.assertEquals(1,controller.settingSelectOptions.size()); 37 | 38 | // User save current setting d1 after deleted group 39 | controller.selectedGroup = 'Marketing'; 40 | controller.groupToDelete = controller.selectedGroup; 41 | controller.deleteERDGroup(); 42 | controller.saveSelectedSetting(); 43 | //List groups = controller.getGroups(); 44 | //System.debug(LoggingLevel.ERROR,'@@@@@@@@@@controller.getGroups():' + groups); 45 | System.assertEquals(3, controller.erdGroups.size(), 'erd map size should be 3 after Marketing deletion'); 46 | 47 | // User type in new setting name and save current setting as new 48 | controller.newSettingName = 'd2'; 49 | controller.saveAsNewSetting(); 50 | System.assertEquals('d2', controller.selectedSetting); 51 | docs = [SELECT Id, Body FROM Document WHERE Name = 'd2' AND FolderId = :controller.persister.folderId]; 52 | System.assertEquals(1, docs.size()); 53 | 54 | // User delete selected setting 55 | controller.deleteSelectedSetting(); 56 | System.assertEquals(null,controller.selectedSetting); 57 | docs = [SELECT Id, Body FROM Document WHERE Name = 'd2' AND FolderId = :controller.persister.folderId]; 58 | System.assertEquals(0, docs.size()); 59 | 60 | Test.stopTest(); 61 | } 62 | 63 | @isTest 64 | static void testGroupConfiguration() 65 | { 66 | // Initiate page reference 67 | PageReference pageRef = Page.ERDGeneratorPage; 68 | Test.setCurrentPage(pageRef); 69 | 70 | // Instantiate a new controller with all parameters in the page 71 | ERDGeneratorController controller; 72 | controller = new ERDGeneratorController(); 73 | controller.ensureDefaultDiagram(); 74 | 75 | Test.startTest(); 76 | // User unselect a group 77 | controller.erdGroups[0].selected = false; 78 | controller.saveSelectedSetting(); 79 | System.assertEquals(3, controller.settings.includedGroups.size()); 80 | 81 | // User select a group 82 | controller.erdGroups[0].selected = true; 83 | controller.saveSelectedSetting(); 84 | System.assertEquals(4, controller.settings.includedGroups.size()); 85 | 86 | // User add a new group 87 | controller.newGroupName = 'New Group'; 88 | controller.addNewGroupWithInput(); 89 | controller.saveSelectedSetting(); 90 | System.assertEquals(5, controller.settings.includedGroups.size()); 91 | 92 | // User delete a group 93 | controller.groupToDelete = 'New Group'; 94 | controller.deleteERDGroup(); 95 | controller.saveSelectedSetting(); 96 | System.assertEquals(4, controller.settings.includedGroups.size()); 97 | 98 | // User press edit button on a group 99 | controller.selectedGroup = 'Common'; 100 | String selectedObjectsForGroup = controller.getSelectedObjects(); 101 | System.assertEquals('Contact,Account', selectedObjectsForGroup); 102 | String objectsForSelectedGroup = controller.getObjectsForSelectedGroup(); 103 | System.assertEquals(true, objectsForSelectedGroup.contains('Contact')); 104 | 105 | // User add objects to a group 106 | controller.setSelectedObjects('Contact,Account,Lead'); 107 | controller.saveSelectedSetting(); 108 | selectedObjectsForGroup = controller.getSelectedObjects(); 109 | System.assertEquals('Contact,Account,Lead',selectedObjectsForGroup); 110 | 111 | // User remove objects from a group 112 | controller.setSelectedObjects('Contact,Account'); 113 | controller.saveSelectedSetting(); 114 | selectedObjectsForGroup = controller.getSelectedObjects(); 115 | System.assertEquals('Contact,Account',selectedObjectsForGroup); 116 | 117 | Test.stopTest(); 118 | } 119 | 120 | 121 | @isTest 122 | static void testObjectConfiguration() 123 | { 124 | // Initiate page reference 125 | PageReference pageRef = Page.ERDGeneratorPage; 126 | Test.setCurrentPage(pageRef); 127 | 128 | // Instantiate a new controller with all parameters in the page 129 | ERDGeneratorController controller; 130 | controller = new ERDGeneratorController(); 131 | controller.ensureDefaultDiagram(); 132 | 133 | // User press edit button on a group 134 | controller.selectedGroup = 'Common'; 135 | String selectedObjectsForGroup = controller.getSelectedObjects(); 136 | System.assertEquals('Contact,Account', selectedObjectsForGroup); 137 | 138 | // User add objects to a group 139 | controller.setSelectedObjects('Contact,Account,Lead'); 140 | selectedObjectsForGroup = controller.getSelectedObjects(); 141 | System.assertEquals('Contact,Account,Lead',selectedObjectsForGroup); 142 | 143 | Test.startTest(); 144 | 145 | // User switch to step 3 - object > fields section 146 | controller.refreshObjectFieldSelection(); 147 | Set allSelectedObjects = new Set(controller.getAllSelectedObjects()); 148 | System.assertEquals(true,allSelectedObjects.contains('Lead')); 149 | 150 | // User press edit button on an object 151 | controller.selectedSingleObject = 'Contact'; 152 | String allFieldsForSelectedObject = controller.getAllFieldsForSelectedObject(); 153 | String selectedObjectFields = controller.getSelectedObjectFields(); 154 | System.assertEquals(true,allFieldsForSelectedObject.contains('FirstName')); 155 | System.assertEquals(true,selectedObjectFields.contains('FirstName')); 156 | System.assertEquals(false,selectedObjectFields.contains('Email')); 157 | 158 | // User add fields to an object 159 | controller.setSelectedObjectFields('Jigsaw,Languages__c,FirstName,LastName,Email'); 160 | controller.saveSelectedSetting(); 161 | selectedObjectFields = controller.getSelectedObjectFields(); 162 | System.assertEquals(true,selectedObjectFields.contains('Email')); 163 | 164 | // User remove fields from an object 165 | controller.setSelectedObjectFields('Jigsaw,Languages__c,FirstName,LastName'); 166 | controller.saveSelectedSetting(); 167 | selectedObjectFields = controller.getSelectedObjectFields(); 168 | System.assertEquals(false,selectedObjectFields.contains('Email')); 169 | 170 | Test.stopTest(); 171 | } 172 | 173 | 174 | @isTest 175 | static void testSettingOptions() 176 | { 177 | // Initiate page reference 178 | PageReference pageRef = Page.ERDGeneratorPage; 179 | Test.setCurrentPage(pageRef); 180 | 181 | // Instantiate a new controller with all parameters in the page 182 | ERDGeneratorController controller; 183 | controller = new ERDGeneratorController(); 184 | controller.ensureDefaultDiagram(); 185 | 186 | Test.startTest(); 187 | 188 | // Test include fields with default diagram 189 | Boolean includeFields = controller.getIncludeFields(); 190 | Boolean includeStandardUserLookup = controller.getIncludeStandardUserLookup(); 191 | System.assertEquals(true,includeFields); 192 | 193 | // User diselect include fields checkbox 194 | controller.setIncludeFields(false); 195 | includeFields = controller.getIncludeFields(); 196 | controller.saveSelectedSetting(); 197 | System.assertEquals(false,includeFields); 198 | 199 | // User select include standard user lookup 200 | controller.setIncludeStandardUserLookup(true); 201 | includeStandardUserLookup = controller.getIncludeStandardUserLookup(); 202 | controller.saveSelectedSetting(); 203 | System.assertEquals(true,includeStandardUserLookup); 204 | 205 | Test.stopTest(); 206 | } 207 | 208 | @isTest 209 | static void testERDContentGeneration() 210 | { 211 | // Initiate page reference 212 | PageReference pageRef = Page.ERDGeneratorPage; 213 | Test.setCurrentPage(pageRef); 214 | 215 | // Instantiate a new controller with all parameters in the page 216 | ERDGeneratorController controller; 217 | controller = new ERDGeneratorController(); 218 | controller.ensureDefaultDiagram(); 219 | 220 | Test.startTest(); 221 | 222 | // Test templates list: 223 | List templates = controller.getTemplates(); 224 | System.assertEquals(2, templates.size()); 225 | 226 | // User press generate button 227 | controller.template = 'DefaultTemplate'; 228 | controller.generateERD(); 229 | System.assertEquals(true,controller.generatedContent.contains('graph [rankdir=LR,nodesep=1.0];')); 230 | System.assertEquals(true,controller.generatedContent.contains('label = "Common"')); 231 | System.assertEquals(true,controller.generatedContent.contains('Account -> Account')); 232 | System.assertEquals(true,controller.generatedContent.contains('Account [label=')); 233 | Test.stopTest(); 234 | } 235 | 236 | @isTest 237 | static void testERDPersisterMethods() 238 | { 239 | // Instantiate a new controller with all parameters in the page 240 | ERDGeneratorController controller; 241 | controller = new ERDGeneratorController(); 242 | 243 | Test.startTest(); 244 | 245 | // On controller initialisation in new org, no settings available 246 | List settingNames = controller.persister.getSettingsNameList(); 247 | System.assertEquals(0,settingNames.size()); 248 | 249 | // Controller initialise with DefaultDiagram 250 | controller.ensureDefaultDiagram(); 251 | settingNames = controller.persister.getSettingsNameList(); 252 | System.assertEquals(1,settingNames.size()); 253 | 254 | // Controller load settings 255 | controller.loadSelectedSetting(); 256 | System.assertEquals(4,controller.erdGroups.size()); 257 | 258 | Test.stopTest(); 259 | } 260 | } -------------------------------------------------------------------------------- /src/classes/ERDGeneratorControllerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDGeneratorTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | private with sharing class ERDGeneratorTest 3 | { 4 | /* 5 | @isTest 6 | static void testThatTemplatesGenerateExpectedOutput() { 7 | 8 | ERDGenerator generator = new ERDGenerator('DefaultTemplate'); 9 | 10 | Map> basicEntitiesWithValidAPINames = new Map>{ 11 | 'Standard' => new List{'Account', 'Contact'} 12 | }; 13 | 14 | Map> basicEntitiesWithInvalidAPINames = new Map>{ 15 | 'Standard' => new List{'account', 'contact'} 16 | }; 17 | 18 | Test.startTest(); 19 | String validERDOutput = generator.generateERD(basicEntitiesWithValidAPINames,false); 20 | String invalidERDOutput = generator.generateERD(basicEntitiesWithInvalidAPINames,false); 21 | Test.stopTest(); 22 | 23 | System.assertNotEquals(-1, validERDOutput.indexOf('->'), 24 | 'There should be at least one relationship between the two entities'); 25 | 26 | System.assertEquals(-1, invalidERDOutput.indexOf('->'), 27 | 'There should be NO relationship between the two entities because they have invalid API names'); 28 | }*/ 29 | 30 | @isTest 31 | static void testThatTemplatesGenerateExpectedOutput() 32 | { 33 | ERDGenerator generator = new ERDGenerator('DefaultTemplate'); 34 | 35 | ERDSettingsV3 settings = new ERDSettingsV3(); 36 | settings.groupToObjectsToFieldsMap = new Map>>{ 37 | 'Standard' => new Map>{ 38 | 'Account' => new List(), 39 | 'Contact' => new List() 40 | } 41 | }; 42 | settings.includedGroups = new Set{'Standard'}; 43 | settings.includeFields = true; 44 | settings.includeStandardUserLookup = false; 45 | 46 | Test.startTest(); 47 | String erdOutput = generator.generateERD(settings); 48 | Test.stopTest(); 49 | 50 | System.assertNotEquals(-1, erdOutput.indexOf('->'), 51 | 'There should be at least one relationship between the two entities'); 52 | } 53 | 54 | @isTest 55 | static void testThatTemplatesGenerateNoRelationshipWithInvalidAPINames() 56 | { 57 | ERDGenerator generator = new ERDGenerator('DefaultTemplate'); 58 | 59 | ERDSettingsV3 settings = new ERDSettingsV3(); 60 | settings.groupToObjectsToFieldsMap = new Map>>{ 61 | 'Standard' => new Map>{ 62 | 'account' => new List(), 63 | 'contact' => new List() 64 | } 65 | }; 66 | settings.includedGroups = new Set{'Standard'}; 67 | settings.includeFields = true; 68 | settings.includeStandardUserLookup = false; 69 | 70 | Test.startTest(); 71 | String erdOutput = generator.generateERD(settings); 72 | Test.stopTest(); 73 | 74 | System.assertEquals(-1, erdOutput.indexOf('->'), 75 | 'There should be NO relationship between the two entities because they have invalid API names'); 76 | } 77 | } -------------------------------------------------------------------------------- /src/classes/ERDGeneratorTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDInspector.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * ERD inspector to convert schema data into Graphviz content 3 | */ 4 | public with sharing class ERDInspector 5 | { 6 | 7 | public void inspectERDSchema(Set objectNameSet, 8 | Map>> erdMap, 9 | Map> groupToERDEntityListMap, 10 | List allObjectRelationshipList 11 | ) 12 | { 13 | Map objectNameSObjectResultMap = getObjectNameSObjectResultMap(objectNameSet); 14 | 15 | for(String grp : erdMap.keySet()) 16 | { 17 | List erdEntityList = new List(); 18 | Map> objectToFieldsMap = erdMap.get(grp); 19 | for(String objectName : objectToFieldsMap.keySet()) 20 | { 21 | Schema.DescribeSobjectResult objectResult = objectNameSObjectResultMap.get(objectName); 22 | 23 | // Get list of ERDEntityField 24 | List includedFieldNames = objectToFieldsMap.get(objectName); 25 | includedFieldNames.sort(); 26 | List erdFields = getIncludedERDEntityFieldListByObjectName(objectName,includedFieldNames); 27 | 28 | // Add ERDEntity to Map 29 | ERDEntity entity = new ERDEntity(); 30 | entity.objectName = objectName; 31 | entity.isCustom = objectResult.isCustom(); 32 | entity.fieldList = erdFields; 33 | erdEntityList.add(entity); 34 | 35 | // For all the child relationship of an object 36 | List childRelationshipForObject = objectResult.getChildRelationships(); 37 | for(Schema.ChildRelationship childRelationship : childRelationshipForObject) 38 | { 39 | // Get the child relationship object name 40 | Schema.Sobjecttype childObjectType = childRelationship.getChildSObject(); 41 | Schema.DescribeSObjectResult childObjectResult = childObjectType.getDescribe(); 42 | String childName = childObjectResult.getName(); 43 | 44 | // Ensure the child object is in the objectNameSet, meaning it is specified by user in the UI 45 | // Ensure the relationship has not been added 46 | if(objectNameSet.contains(childName) && !ERDUtils.isEnitityRelationshipExists(objectName,childName,allObjectRelationshipList)) 47 | { 48 | /* 49 | System.debug(LoggingLevel.ERROR, '@@@@@@@@@@@@@childRelationship:' + childRelationship.getChildSObject()); 50 | System.debug(LoggingLevel.ERROR, '@@@@@@@@@@@@@childRelationship:' + childRelationship.getField()); 51 | System.debug(LoggingLevel.ERROR, '@@@@@@@@@@@@@childRelationship:' + childRelationship.getRelationshipName()); 52 | System.debug(LoggingLevel.ERROR, '@@@@@@@@@@@@@childRelationship:' + childRelationship.isCascadeDelete()); 53 | System.debug(LoggingLevel.ERROR, '@@@@@@@@@@@@@childRelationship:' + childRelationship.isDeprecatedAndHidden()); 54 | System.debug(LoggingLevel.ERROR, '@@@@@@@@@@@@@childRelationship:' + childRelationship.isRestrictedDelete()); 55 | */ 56 | 57 | // Add ERDEntityRelationship to List of relationsihp for an object 58 | ERDEntityRelationship entityRelationship = new ERDEntityRelationship(); 59 | entityRelationship.lookupFieldName = childRelationship.getField().getDescribe().getName(); 60 | entityRelationship.parentObjectName = objectName; 61 | entityRelationship.childObjectName = childName; 62 | entityRelationship.isCascadeDelete = childRelationship.isCascadeDelete(); 63 | allObjectRelationshipList.add(entityRelationship); 64 | } 65 | } 66 | } 67 | groupToERDEntityListMap.put(grp,erdEntityList); 68 | 69 | } 70 | 71 | } 72 | 73 | 74 | private Map getObjectNameSObjectResultMap(Set objectNameSet) 75 | { 76 | Map objectNameSObjectResultMap = new Map(); 77 | List allObjectResults = Schema.describeSObjects(new List(objectNameSet)); 78 | 79 | for(String objectName : objectNameSet) 80 | { 81 | for(Schema.DescribeSobjectResult objectResult : allObjectResults) 82 | { 83 | if(objectResult.getName() == objectName) 84 | { 85 | objectNameSObjectResultMap.put(objectName, objectResult); 86 | break; 87 | } 88 | } 89 | } 90 | 91 | return objectNameSObjectResultMap; 92 | } 93 | 94 | public List getFieldNamesByObjectName(String objectName) 95 | { 96 | List fieldNames = new List(); 97 | List objectResults = Schema.describeSObjects(new List{objectName}); 98 | if(!objectResults.isEmpty()) 99 | { 100 | Map objectFieldMap = objectResults[0].fields.getMap(); 101 | List fieldValues = objectFieldMap.values(); 102 | 103 | for(Schema.SObjectField field : fieldValues) 104 | { 105 | Schema.DescribeFieldResult fieldResult = field.getDescribe(); 106 | fieldNames.add(fieldResult.getName()); 107 | } 108 | } 109 | fieldNames.sort(); 110 | return fieldNames; 111 | } 112 | 113 | public List getAvailableObjectsForGroup(String selectedGroup, 114 | List allObjectNames, 115 | Map>> erdMap) 116 | { 117 | Set availableObjects = new Set(allObjectNames); 118 | 119 | for(String grp : erdMap.keySet()) 120 | { 121 | if(grp != selectedGroup) 122 | { 123 | for(String obj : erdMap.get(grp).keySet()) 124 | { 125 | if(availableObjects.contains(obj)) 126 | { 127 | availableObjects.remove(obj); 128 | } 129 | } 130 | } 131 | } 132 | 133 | List sorted = new List(availableObjects); 134 | sorted.sort(); 135 | return sorted; 136 | } 137 | 138 | /****************Local Functions*****************/ 139 | private List getIncludedERDEntityFieldListByObjectName(String objectName, List includedFieldNames) 140 | { 141 | List erdFields = new List(); 142 | //Set includedFieldNameSet = new Set(includedFieldNames); 143 | Map fieldNameToFieldTypeMap = new Map(); 144 | List objectResults = Schema.describeSObjects(new List{objectName}); 145 | 146 | if(!objectResults.isEmpty()) 147 | { 148 | // collect field schema 149 | Map objectFieldMap = objectResults[0].fields.getMap(); 150 | List fieldValues = objectFieldMap.values(); 151 | 152 | // Setup field name to field type map 153 | for(Schema.SObjectField field : fieldValues) 154 | { 155 | Schema.DescribeFieldResult fieldResult = field.getDescribe(); 156 | String fieldName = fieldResult.getName(); 157 | Schema.DisplayType displayType= fieldResult.getType(); 158 | String fieldType = ERDDisplayType.getDisplayTypeLabel(displayType); 159 | fieldNameToFieldTypeMap.put(fieldName,fieldType); 160 | } 161 | 162 | for(String includedFieldName : includedFieldNames) 163 | { 164 | ERDEntityField erdField = new ERDEntityField(includedFieldName,fieldNameToFieldTypeMap.get(includedFieldName)); 165 | erdFields.add(erdField); 166 | } 167 | } 168 | 169 | return erdFields; 170 | } 171 | } -------------------------------------------------------------------------------- /src/classes/ERDInspector.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDListUtils.cls: -------------------------------------------------------------------------------- 1 | /* 2 | Name: ERDListUtils 3 | Purpose: 4 | This class is to provide utility functionalities for List Collection primitive 5 | */ 6 | public with sharing class ERDListUtils 7 | { 8 | public static Integer getStringItemIndex(String item, List items) 9 | { 10 | if(items != null) 11 | { 12 | for(Integer i=0;i 2 | 3 | 30.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDPersister.cls: -------------------------------------------------------------------------------- 1 | public with sharing class ERDPersister { 2 | 3 | public static final Integer CURRENT_VERSION = 3; 4 | @TestVisible private static final String FOLDER_ERD_SETTINGS = 'ERD_Settings'; 5 | 6 | public interface Versionable { 7 | Integer getVersion(); 8 | void upgrade(Versionable old); 9 | } 10 | 11 | public Id folderId; 12 | 13 | public ERDPersister() { 14 | List fs = [select Id from Folder 15 | where DeveloperName = :FOLDER_ERD_SETTINGS 16 | and Type = 'Document']; 17 | if (fs.size() == 0) { 18 | throw new ERDException('Folder not found: '+FOLDER_ERD_SETTINGS); 19 | } else if (fs.size() == 1) { 20 | this.folderId = fs.get(0).Id; 21 | } else { 22 | throw new ERDException('Too many folders found: '+FOLDER_ERD_SETTINGS); 23 | } 24 | } 25 | 26 | public List getSettingsNameList() 27 | { 28 | List diagramList = new List(); 29 | if(folderId != null) { 30 | List ds = [SELECT Id, Name FROM Document WHERE folderId = :folderId]; 31 | 32 | if(!ds.isEmpty()) { 33 | for(Document doc : ds) { 34 | diagramList.add(doc.Name); 35 | } 36 | } 37 | } 38 | return diagramList; 39 | } 40 | 41 | public Versionable getDefaultSettings() { 42 | StaticResource defaultDiagram = [select Body from StaticResource where Name = 'DefaultDiagram']; 43 | return upgrade(3, defaultDiagram.Body.toString()); 44 | } 45 | 46 | public Versionable getSettings(String name) 47 | { 48 | return getSettingsByNameVersion(name,CURRENT_VERSION); 49 | } 50 | 51 | public Versionable getSettings(String name, Integer version) 52 | { 53 | return getSettingsByNameVersion(name,version); 54 | } 55 | 56 | /* returns the settings upgraded to the latest version expected by the code base. 57 | This provides a way for existing settings to be automatically upgraded when they are read */ 58 | private Versionable getSettingsByNameVersion(String name, Integer version) { 59 | List ds = [select Id, Body from Document 60 | where Name = :name 61 | and FolderId = :folderId]; 62 | 63 | if (ds.size() == 0) { 64 | return null; 65 | } else if (ds.size() == 1) { 66 | String settingJSON = ds.get(0).Body.toString(); 67 | Versionable v = upgrade(version, settingJSON); 68 | return v; 69 | } else { 70 | throw new ERDException('Too many documents found: '+name); 71 | } 72 | } 73 | 74 | // separated for testability 75 | public Versionable upgrade(Integer targetVersion, String persisted) { 76 | Map persistedMap = (Map) JSON.deserializeUntyped(persisted); 77 | Integer persistedVersion = (Integer) persistedMap.get('version'); 78 | 79 | System.debug(LoggingLevel.ERROR, '====>persistedVersion:' + persistedVersion); 80 | System.debug(LoggingLevel.ERROR, '====>persisted:' + persisted); 81 | if (persistedVersion == null) {// prior to versionable, only the groups were saved 82 | 83 | Versionable setting; 84 | 85 | if(persistedMap != null && !persistedMap.isEmpty()) 86 | { 87 | if(persistedMap.values()[0] instanceof List) 88 | { 89 | System.debug(LoggingLevel.ERROR, '====>old version'); 90 | ERDSettingsV1 v1 = new ERDSettingsV1(); 91 | v1.groups = new Map>(); 92 | for (String g : persistedMap.keySet()) { 93 | v1.groups.put(g, new List()); 94 | List objects = (List) persistedMap.get(g); 95 | for (Object o : objects) { 96 | v1.groups.get(g).add((String) o); 97 | } 98 | } 99 | 100 | setting = v1; 101 | } 102 | else 103 | { 104 | System.debug(LoggingLevel.ERROR, '====>new version'); 105 | ERDSettingsV3 v3 = new ERDSettingsV3(); 106 | v3.groupToObjectsToFieldsMap = new Map>>(); 107 | for(String g : persistedMap.keySet()) 108 | { 109 | System.debug(LoggingLevel.ERROR, '====>g:' + g); 110 | v3.groupToObjectsToFieldsMap.put(g, new Map>()); 111 | Map objectToFieldsMap = (Map)persistedMap.get(g); 112 | System.debug(LoggingLevel.ERROR, '====>objectToFieldsMap:' + objectToFieldsMap); 113 | 114 | for(String objectName : objectToFieldsMap.keySet()) 115 | { 116 | v3.groupToObjectsToFieldsMap.get(g).put(objectName, new List()); 117 | 118 | List objects = (List) objectToFieldsMap.get(objectName); 119 | 120 | System.debug(LoggingLevel.ERROR, '====>objects:' + objects); 121 | for (Object o : objects) 122 | { 123 | v3.groupToObjectsToFieldsMap.get(g).get(objectName).add((String) o); 124 | } 125 | } 126 | } 127 | 128 | setting = v3; 129 | } 130 | } 131 | 132 | 133 | return upgrade(targetVersion, setting); 134 | } else { 135 | Type persistedType = Type.forName('ERDSettingsV'+persistedVersion); 136 | Versionable hydrated = (Versionable) JSON.deserialize(persisted, persistedType); 137 | System.debug(LoggingLevel.ERROR, '====>hydrated:' + hydrated); 138 | // Until this point, the deserialized 139 | return upgrade(targetVersion, hydrated); 140 | } 141 | } 142 | 143 | public Versionable upgrade(Integer targetVersion, Versionable oldVersion) { 144 | Versionable latest = oldVersion; 145 | while (latest.getVersion() < targetVersion) { 146 | System.debug(LoggingLevel.ERROR, '@@@@upgrading:' + latest.getVersion() + ',' + targetVersion); 147 | Type upgradedType = Type.forName('ERDSettingsV'+(latest.getVersion()+1)); 148 | Versionable nextVersion = (Versionable) upgradedType.newInstance(); 149 | nextVersion.upgrade(latest); 150 | latest = nextVersion; 151 | } 152 | return latest; 153 | } 154 | 155 | public void saveSettings(String name, Versionable settings) { 156 | System.debug(LoggingLevel.ERROR, '@@@@saveSettings is called'); 157 | List ds = [select Id from Document 158 | where Name = :name 159 | and FolderId = :folderId]; 160 | 161 | String body = JSON.serialize(settings); 162 | 163 | if (ds.size() == 0) { 164 | Document d = new Document( 165 | Name = name, 166 | FolderId = folderId, 167 | Body = Blob.valueOf(body) 168 | ); 169 | insert d; 170 | } else if (ds.size() == 1) { 171 | Document d = ds.get(0); 172 | d.Body = Blob.valueOf(body); 173 | update d; 174 | } else { 175 | throw new ERDException('Too many Documents found: '+name); 176 | } 177 | 178 | } 179 | 180 | public void deleteSettings(String name) 181 | { 182 | List ds = [select Id from Document 183 | where Name = :name 184 | and FolderId = :folderId]; 185 | 186 | if(ds.size() == 1) 187 | { 188 | Document d = ds.get(0); 189 | delete d; 190 | } 191 | else 192 | { 193 | throw new ERDException('Document does not exist: ' + name); 194 | } 195 | } 196 | 197 | 198 | } -------------------------------------------------------------------------------- /src/classes/ERDPersister.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDPersisterTests.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class ERDPersisterTests { 3 | 4 | 5 | @isTest 6 | private static void testUpgrade() { 7 | ERDPersister persister = new ERDPersister(); 8 | 9 | Map> v0 = new Map>{ 10 | 'Sales' => new List{'Account', 'Contact'}, 11 | 'Marketing' => new List{'Lead', 'Campaign'} 12 | }; 13 | 14 | String v0Persisted = JSON.serialize(v0); 15 | 16 | Test.startTest(); 17 | // Test upgrade to v1 18 | ERDSettingsV1 v1 = (ERDSettingsV1) persister.upgrade(1, v0Persisted); 19 | System.assertEquals(v0, v1.groups, 'groups should upgrade from v0 -> v1'); 20 | 21 | // Test upgrade to v2 22 | ERDSettingsV2 v2 = (ERDSettingsV2) persister.upgrade(2, JSON.serialize(v1)); 23 | System.assertEquals(v0, v2.groups, 'groups should upgrade from v1 -> v2'); 24 | System.assertEquals(false, v2.includeFields, 'fields should have correct default'); 25 | 26 | // Test upgrade to v3 27 | Map>> v3Map = new Map>>{ 28 | 'Sales' => new Map> 29 | { 30 | 'Account' => new List(), 31 | 'Contact' => new List() 32 | }, 33 | 'Marketing' => new Map> 34 | { 35 | 'Lead' => new List(), 36 | 'Campaign' => new List() 37 | } 38 | }; 39 | Set v3IncludedGroups = v2.groups.keySet(); 40 | ERDSettingsv3 v3 = (ERDSettingsV3) persister.upgrade(3, JSON.serialize(v2)); 41 | Test.stopTest(); 42 | 43 | System.assertEquals(v3Map, v3.groupToObjectsToFieldsMap, 'groupToObjectsToFieldsMap should be constructed from v2.groups'); 44 | System.assertEquals(v3IncludedGroups, v3.includedGroups, 'includedGroups should contains all groups from v2'); 45 | System.assertEquals(false, v3.includeFields, 'fields should have correct default'); 46 | } 47 | 48 | @isTest 49 | private static void testRoundTrip() { 50 | 51 | String name1 = 'd1'; 52 | 53 | ERDPersister persister = new ERDPersister(); 54 | 55 | Map> v0 = new Map>{ 56 | 'Sales' => new List{'Account', 'Contact'}, 57 | 'Marketing' => new List{'Lead', 'Campaign'} 58 | }; 59 | 60 | // create fake non-versioned persisted settings 61 | insert new Document(Name = name1, 62 | FolderId = persister.folderId, 63 | Body = Blob.valueOf(JSON.serialize(v0))); 64 | 65 | // Test ERDSettingsV2 66 | Test.startTest(); 67 | ERDSettingsV2 v2Retrieved = (ERDSettingsV2) persister.getSettings(name1,2); 68 | System.assertEquals(v0, v2Retrieved.groups, 69 | 'Groups survive the upgrade and are copied into the latest version'); 70 | 71 | v2Retrieved.groups.get('Sales').add('Opportunity'); 72 | v2Retrieved.includeFields = true; 73 | 74 | persister.saveSettings(name1, v2Retrieved); 75 | 76 | ERDSettingsV2 v2RetrievedAgain = (ERDSettingsV2) persister.getSettings(name1,2); 77 | System.assertEquals(v2Retrieved.groups, v2RetrievedAgain.groups, 78 | 'An updated diagram retrieved later matches the original'); 79 | System.assertEquals(v2Retrieved.includeFields, v2RetrievedAgain.includeFields, 80 | 'An updated diagram retrieved later matches the original'); 81 | 82 | 83 | // Test ERDSettingsV3 84 | Map>> v3MapToTest = new Map>>{ 85 | 'Sales' => new Map> 86 | { 87 | 'Account' => new List(), 88 | 'Contact' => new List(), 89 | 'Opportunity' => new List() 90 | }, 91 | 'Marketing' => new Map> 92 | { 93 | 'Lead' => new List(), 94 | 'Campaign' => new List() 95 | } 96 | }; 97 | 98 | ERDSettingsV3 v3Retrieved = (ERDSettingsV3) persister.getSettings(name1); 99 | System.assertEquals(v3MapToTest, v3Retrieved.groupToObjectsToFieldsMap,'Groups survive the upgrade and are copied into the latest version'); 100 | v3Retrieved.groupToObjectsToFieldsMap.put('Finance', new Map>()); 101 | v3Retrieved.groupToObjectsToFieldsMap.get('Sales').put('Opportunity',new List()); 102 | v3Retrieved.groupToObjectsToFieldsMap.get('Sales').get('Account').add('Name'); 103 | v3Retrieved.includedGroups = new Set(); 104 | v3Retrieved.includedGroups.add('Finance'); 105 | v3Retrieved.includeFields = true; 106 | 107 | persister.saveSettings(name1, v3Retrieved); 108 | 109 | ERDSettingsV3 v3RetrievedAgain = (ERDSettingsV3) persister.getSettings(name1); 110 | System.assertEquals(v3Retrieved.groupToObjectsToFieldsMap, v3RetrievedAgain.groupToObjectsToFieldsMap, 'An updated diagram retrieved later matches the original'); 111 | System.assertEquals(v3Retrieved.includedGroups, v3RetrievedAgain.includedGroups, 'An updated diagram retrieved later matches the original'); 112 | System.assertEquals(v3Retrieved.includeFields, v3RetrievedAgain.includeFields, 'An updated diagram retrieved later matches the original'); 113 | 114 | Test.stopTest(); 115 | } 116 | } -------------------------------------------------------------------------------- /src/classes/ERDPersisterTests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDSettingProcessor.cls: -------------------------------------------------------------------------------- 1 | public with sharing class ERDSettingProcessor 2 | { 3 | public Set retrieveAllObjectsForGroups(ERDSettingsv3 settings) 4 | { 5 | Set objectNameSet = new Set(); 6 | 7 | for(String grp : settings.groupToObjectsToFieldsMap.keySet()) 8 | { 9 | if(settings.includedGroups.contains(grp)) 10 | { 11 | objectNameSet.addAll(settings.groupToObjectsToFieldsMap.get(grp).keySet()); 12 | } 13 | } 14 | 15 | return objectNameSet; 16 | } 17 | 18 | public void setSelectedObjectsToGroup(ERDSettingsV3 settings, String csv, String selectedGroup) 19 | { 20 | Map> objectToFieldsMap = settings.groupToObjectsToFieldsMap.get(selectedGroup); 21 | if(objectToFieldsMap == null) {objectToFieldsMap = new Map>();} 22 | 23 | Map> newObjectToFieldsMap = new Map>(); 24 | 25 | for(String selectedObj : csv.split(',')) 26 | { 27 | if(!String.isBlank(selectedObj)) 28 | { 29 | List selectedObjectList = new List(); 30 | if(objectToFieldsMap.keySet().contains(selectedObj)) 31 | { 32 | selectedObjectList = objectToFieldsMap.get(selectedObj); 33 | } 34 | newObjectToFieldsMap.put(selectedObj, selectedObjectList); 35 | } 36 | } 37 | settings.groupToObjectsToFieldsMap.put(selectedGroup, newObjectToFieldsMap); 38 | } 39 | 40 | public List retrieveFieldsForObject(ERDSettingsV3 settings, String selectedSingleObject) 41 | { 42 | for(Map> objectToFieldsMap : settings.groupToObjectsToFieldsMap.values()) 43 | { 44 | for(String obj : objectToFieldsMap.keySet()) 45 | { 46 | if(obj == selectedSingleObject) 47 | { 48 | return objectToFieldsMap.get(obj); 49 | } 50 | } 51 | } 52 | return null; 53 | } 54 | 55 | public void setSelectedFieldsToObject(ERDSettingsV3 settings, String csv, String selectedSingleObject) 56 | { 57 | for(Map> objectToFieldsMap : settings.groupToObjectsToFieldsMap.values()) 58 | { 59 | for(String obj : objectToFieldsMap.keySet()) 60 | { 61 | if(obj == selectedSingleObject) 62 | { 63 | Set newFieldsList = new Set(); 64 | for(String selectedField : csv.split(',')) 65 | { 66 | if(!String.isBlank(selectedField)) 67 | { 68 | newFieldsList.add(selectedField); 69 | } 70 | } 71 | objectToFieldsMap.put(obj,new List(newFieldsList)); 72 | return; 73 | } 74 | } 75 | } 76 | } 77 | 78 | public void setERDGroupSelection(ERDSettingsV3 settings, List erdGroups) 79 | { 80 | Set includedGroups = new Set(); 81 | if(erdGroups != null) 82 | { 83 | for(ERDEntityGroup grp : erdGroups) 84 | { 85 | if(grp.selected) 86 | { 87 | includedGroups.add(grp.groupName); 88 | } 89 | } 90 | } 91 | settings.includedGroups = includedGroups; 92 | } 93 | 94 | public void addNewERDGroup(ERDSettingsV3 settings, String newGroupName) 95 | { 96 | settings.groupToObjectsToFieldsMap.put(newGroupName, new Map>()); 97 | settings.includedGroups.add(newGroupName); 98 | } 99 | 100 | public void deleteERDGroup(ERDSettingsV3 settings, String groupToDelete) 101 | { 102 | settings.groupToObjectsToFieldsMap.remove(groupToDelete); 103 | if(settings.includedGroups.contains(groupToDelete)) {settings.includedGroups.remove(groupToDelete);} 104 | } 105 | } -------------------------------------------------------------------------------- /src/classes/ERDSettingProcessor.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDSettingsV1.cls: -------------------------------------------------------------------------------- 1 | public class ERDSettingsV1 implements ERDPersister.Versionable { 2 | 3 | // this field should always be present. It is used to hydrate/upgrade automatically 4 | public Integer version = 1; 5 | public Integer getVersion() { return version; } 6 | 7 | // any other fields persisted are here 8 | public Map> groups {get;set;} 9 | 10 | public void upgrade(ERDPersister.Versionable old) { 11 | // this upgrade is handled by the persister 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/classes/ERDSettingsV1.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDSettingsV2.cls: -------------------------------------------------------------------------------- 1 | public class ERDSettingsV2 implements ERDPersister.Versionable { 2 | 3 | // this field should always be present. It is used to hydrate/upgrade automatically 4 | public Integer version = 2; 5 | public Integer getVersion() { return version; } 6 | 7 | // any other fields persisted are here 8 | public Map> groups { 9 | get { 10 | if (groups == null) groups = new Map>(); 11 | return groups; 12 | } 13 | set; 14 | } 15 | public Boolean includeFields {get;set;} 16 | 17 | public void upgrade(ERDPersister.Versionable old) { 18 | // old version is always the current - 1 19 | ERDSettingsV1 v1 = (ERDSettingsV1) old; 20 | 21 | // copy shared data 22 | groups = v1.groups; 23 | 24 | // set sensible defaults for new data 25 | includeFields = false; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/classes/ERDSettingsV2.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDSettingsV3.cls: -------------------------------------------------------------------------------- 1 | public class ERDSettingsV3 implements ERDPersister.Versionable { 2 | 3 | // this field should always be present. It is used to hydrate/upgrade automatically 4 | public Integer version = 3; 5 | public Integer getVersion() { return version; } 6 | public Boolean includeFields {get;set;} 7 | public Boolean includeStandardUserLookup {get;set;} 8 | //public Set objectNameSet; 9 | 10 | // any other fields persisted are here 11 | public Map>> groupToObjectsToFieldsMap 12 | { 13 | get { 14 | if (groupToObjectsToFieldsMap == null) groupToObjectsToFieldsMap = new Map>>(); 15 | return groupToObjectsToFieldsMap; 16 | } 17 | set; 18 | } 19 | public Set includedGroups 20 | { 21 | get { 22 | if (includedGroups == null) includedGroups = new Set(); 23 | return includedGroups; 24 | } 25 | set; 26 | } 27 | 28 | public void upgrade(ERDPersister.Versionable old) { 29 | // old version is always the current - 1 30 | ERDSettingsV2 v2 = (ERDSettingsV2) old; 31 | 32 | // copy shared data 33 | Map> v2Map = v2.groups; 34 | Map>> v3Map = new Map>>(); 35 | Set v3IncludedGroups = new Set(); 36 | 37 | if(v2Map != null) 38 | { 39 | for(String groupName : v2Map.keySet()) 40 | { 41 | Map> objectToFieldsMap = new Map>(); 42 | 43 | for(String objectName : v2Map.get(groupName)) 44 | { 45 | objectToFieldsMap.put(objectName, new List()); 46 | } 47 | v3Map.put(groupName,objectToFieldsMap); 48 | } 49 | 50 | v3IncludedGroups = v2Map.keySet(); 51 | } 52 | 53 | groupToObjectsToFieldsMap = v3Map; 54 | includedGroups = v3IncludedGroups; 55 | 56 | // set sensible defaults for new data 57 | includeFields = v2.includeFields; 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/classes/ERDSettingsV3.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDTemplateEngine.cls: -------------------------------------------------------------------------------- 1 | public class ERDTemplateEngine { 2 | 3 | public interface TemplateSource { 4 | String getTemplate(); 5 | } 6 | 7 | public static final String BINDINGS = '\\{\\{([^}]*)\\}\\}'; 8 | 9 | public final Map templates; 10 | 11 | public ERDTemplateEngine(TemplateSource source) { 12 | 13 | List templateSplit = source.getTemplate().split('=========='); 14 | List extracted = new List(); 15 | for(String t : templateSplit) { 16 | if (t.length() > 0) { 17 | extracted.add(t.substringAfter('\n')); 18 | } 19 | } 20 | 21 | templates = new Map{ 22 | ERDTemplateType.MAIN => extracted.get(0), 23 | ERDTemplateType.CLUSTER => extracted.get(1), 24 | ERDTemplateType.ENTITY => extracted.get(2), 25 | ERDTemplateType.MD_RELATIONSHIP => extracted.get(3), 26 | ERDTemplateType.LOOKUP_RELATIONSHIP => extracted.get(4), 27 | ERDTemplateType.FIELD => extracted.get(5) 28 | }; 29 | } 30 | 31 | public List getBindExpressions(String template) { 32 | Pattern p = Pattern.compile(BINDINGS); 33 | Matcher m = p.matcher(template); 34 | List results = new List(); 35 | while (m.find()) { 36 | results.add(m.group()); 37 | } 38 | return results; 39 | } 40 | 41 | public String applyBindings(String template, Map data) { 42 | System.debug(LoggingLevel.ERROR, '====> template:' + template); 43 | List binds = getBindExpressions(template); 44 | System.debug(LoggingLevel.ERROR, '====> binds:' + binds); 45 | for (String bind : binds) { 46 | String key = bind.substring(2, bind.length()-2); 47 | String val = data.get(key); 48 | System.debug(LoggingLevel.ERROR, '====> key:' + key); 49 | System.debug(LoggingLevel.ERROR, '====> val:' + val); 50 | template = template.replace(bind, val==null?'':val); 51 | } 52 | return template; 53 | } 54 | 55 | public String render(ERDTemplateType template, Map data) { 56 | 57 | System.debug(LoggingLevel.ERROR, '====> template type:' + template); 58 | System.debug(LoggingLevel.ERROR, '====> data:' + data); 59 | return applyBindings(templates.get(template), data); 60 | } 61 | 62 | public class CalloutTemplateSource implements TemplateSource { 63 | private final String url; 64 | public CalloutTemplateSource(String url) { 65 | this.url = url; 66 | } 67 | public String getTemplate() { 68 | Http http = new Http(); 69 | HttpRequest request = new HttpRequest(); 70 | request.setEndpoint(url); 71 | request.setMethod('GET'); 72 | HttpResponse response = http.send(request); 73 | return response.getBody(); 74 | } 75 | } 76 | 77 | public class StaticResourceTemplateSource implements TemplateSource { 78 | 79 | private final String resourceName; 80 | 81 | public StaticResourceTemplateSource(String resourceName) { 82 | if (resourceName == null) { 83 | throw new ERDException('Resource name not present'); 84 | } 85 | this.resourceName = resourceName; 86 | } 87 | 88 | public String getTemplate() { 89 | 90 | List resources = [select id, name, body from StaticResource 91 | where name = :resourceName]; 92 | if (resources.size() == 1) { 93 | return resources.get(0).body.toString(); 94 | } else { 95 | throw new ERDException('Resource not found: '+resourceName); 96 | } 97 | } 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /src/classes/ERDTemplateEngine.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDTemplateEngineTests.cls: -------------------------------------------------------------------------------- 1 | @isTest(seeAllData=true) // need seeAllData to query the Static Resources. Not best practice but making an exception for this test 2 | private class ERDTemplateEngineTests { 3 | 4 | private static void checkTemplateContains(ERDTemplateEngine engine, ERDTemplateType templateType, String bind) { 5 | System.assert(engine.templates.get(templateType).contains('{{'+bind+'}}'), 6 | templateType+' template has bind expression for '+bind); 7 | } 8 | 9 | @IsTest 10 | static void testAllResourceTemplates() { 11 | // check that all supplied templates have expect bind expressions 12 | for (StaticResource r : [SELECT Name FROM StaticResource]) { 13 | if (r.Name.endsWith('Template')) { 14 | ERDTemplateEngine.TemplateSource src = 15 | new ERDTemplateEngine.StaticResourceTemplateSource(r.Name); 16 | ERDTemplateEngine engine = new ERDTemplateEngine(src); 17 | checkTemplateContains(engine, ERDTemplateType.MAIN, 'content'); 18 | checkTemplateContains(engine, ERDTemplateType.ENTITY, 'name'); 19 | checkTemplateContains(engine, ERDTemplateType.CLUSTER, 'content'); 20 | checkTemplateContains(engine, ERDTemplateType.MD_RELATIONSHIP, 'from'); 21 | checkTemplateContains(engine, ERDTemplateType.MD_RELATIONSHIP, 'to'); 22 | checkTemplateContains(engine, ERDTemplateType.LOOKUP_RELATIONSHIP, 'from'); 23 | checkTemplateContains(engine, ERDTemplateType.LOOKUP_RELATIONSHIP, 'to'); 24 | checkTemplateContains(engine, ERDTemplateType.FIELD, 'name'); 25 | } 26 | } 27 | } 28 | 29 | @IsTest 30 | static void testRelationshipTemplate() { 31 | 32 | ERDTemplateEngine.TemplateSource src = 33 | new ERDTemplateEngine.StaticResourceTemplateSource('DefaultTemplate'); 34 | ERDTemplateEngine engine = new ERDTemplateEngine(src); 35 | 36 | String rendered = engine.render(ERDTemplateType.MD_RELATIONSHIP, new Map{ 37 | 'from' => 'Contact', 38 | 'to' => 'Account' 39 | }); 40 | 41 | 42 | System.assert(rendered.contains('Contact -> Account'), 43 | 'Basic relationship should render with entity names and an arrow'); 44 | 45 | rendered = engine.render(ERDTemplateType.LOOKUP_RELATIONSHIP, new Map{ 46 | 'from' => 'Contact', 47 | 'to' => 'Account' 48 | }); 49 | 50 | System.assert(rendered.contains('Contact -> Account'), 51 | 'Basic relationship should render with entity names and an arrow'); 52 | 53 | } 54 | } -------------------------------------------------------------------------------- /src/classes/ERDTemplateEngineTests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDTemplateType.cls: -------------------------------------------------------------------------------- 1 | public enum ERDTemplateType {MAIN, CLUSTER, ENTITY, MD_RELATIONSHIP, LOOKUP_RELATIONSHIP, FIELD} -------------------------------------------------------------------------------- /src/classes/ERDTemplateType.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/classes/ERDUtils.cls: -------------------------------------------------------------------------------- 1 | /** 2 | Name: ERDUtils 3 | Purpose: 4 | This class is to provide utility functionalities for the ERD Generator 5 | */ 6 | public with sharing class ERDUtils 7 | { 8 | public static Boolean isEnitityRelationshipExists(String parentObjectName, String childObjectName, List relationships) 9 | { 10 | if(relationships != null) 11 | { 12 | for(ERDEntityRelationship relationship : relationships) 13 | { 14 | if(relationship.parentObjectName == parentObjectName && relationship.childObjectName == childObjectName) 15 | { 16 | return true; 17 | } 18 | } 19 | } 20 | 21 | return false; 22 | } 23 | 24 | public static Boolean isStandardUserLookupRelationship(ERDEntityRelationship relationship) 25 | { 26 | //System.debug(LoggingLevel.ERROR, '@@@@@@@@@@@@relationship:' + relationship); 27 | return (relationship.parentObjectName != null && relationship.parentObjectName == 'User' && 28 | (relationship.lookupFieldName != null && (relationship.lookupFieldName == 'OwnerId' || relationship.lookupFieldName == 'CreatedById' || relationship.lookupFieldName == 'LastModifiedById'))); 29 | } 30 | 31 | public static List getAllObjectNames() 32 | { 33 | List objectNames = new List(); 34 | 35 | Map globalDescribe = Schema.getGlobalDescribe(); 36 | 37 | for(Schema.SObjectType sObjectType : globalDescribe.values()) 38 | { 39 | Schema.DescribeSObjectResult sObjectResult = sObjectType.getDescribe(); 40 | objectNames.add(sObjectResult.getName()); 41 | } 42 | 43 | objectNames.sort(); 44 | return objectNames; 45 | } 46 | 47 | public static List getERDEntityGroups(Set groups, Set includedGroups) 48 | { 49 | List erdGroups = new List(); 50 | 51 | List sortedGroups = new List(groups); 52 | sortedGroups.sort(); 53 | 54 | for(String g : sortedGroups) 55 | { 56 | ERDEntityGroup grp = new ERDEntityGroup(g,includedGroups.contains(g)); 57 | erdGroups.add(grp); 58 | } 59 | return erdGroups; 60 | } 61 | } -------------------------------------------------------------------------------- /src/classes/ERDUtils.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /src/components/JQuerySelectable.component: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 21 | 22 |
23 |
    24 |
    25 |
    26 | 27 | 65 | 66 | -------------------------------------------------------------------------------- /src/components/JQuerySelectable.component-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | This is a generic template for Visualforce Component. With this template, you may adjust the default elements and values and add new elements and values. 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/objects/ERD__c.object: -------------------------------------------------------------------------------- 1 | 2 | 3 | List 4 | Protected 5 | Saved configs for the ERD Generator 6 | false 7 | 8 | Value__c 9 | false 10 | 11 | 255 12 | true 13 | false 14 | Text 15 | false 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | * 5 | ApexClass 6 | 7 | 8 | JQuerySelectable 9 | ApexComponent 10 | 11 | 12 | * 13 | ApexPage 14 | 15 | 16 | ERD__c 17 | CustomObject 18 | 19 | 20 | * 21 | CustomTab 22 | 23 | 24 | * 25 | StaticResource 26 | 27 | 31.0 28 | 29 | -------------------------------------------------------------------------------- /src/pages/ERDDownloader.page: -------------------------------------------------------------------------------- 1 | 3 | {!generatedContent} 4 | -------------------------------------------------------------------------------- /src/pages/ERDDownloader.page-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | false 5 | false 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/ERDGeneratorPage.page: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 27 | 28 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 |
    147 |

    Diagram: {!currentSetting}

    148 | 151 | 152 | 153 | 154 | Working.. 155 | 156 | 157 | Help 158 |
    159 |
    160 |
    161 | 162 | 164 | 165 |
    166 | 167 | 168 | 169 | 170 | 172 | 173 | 174 | 175 | 178 | 179 | 180 | 181 | 182 | 184 | 185 | 186 | 187 | 188 | 189 |
    190 |
    191 | 192 | 193 |
    194 | 195 | 196 | 197 | 199 | 200 | 201 |
    202 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
    223 |
    224 |
    225 | 226 |
    227 | 228 | 229 | Loading.. 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 |

    Group: {!selectedGroup}

    239 | Hold down Ctrl/Cmd to Select Multiple Objects 240 |
    241 |
    242 | 245 |
    246 |
    247 |
    248 |
    249 |
    250 |
    251 | 252 | 253 |
    254 | 255 | 256 | Loading.. 257 | 258 | 259 | 260 |
    261 | 262 | 263 | 264 | 265 | 266 | 268 | 270 | 271 | 272 | 273 |
    274 |
    275 |
    276 |
    277 |
    278 | 279 |
    280 | 281 | 282 | Loading.. 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 |

    Object: {!selectedSingleObject}

    291 | Hold down Ctrl/Cmd to Select Multiple Fields 292 |
    293 |
    294 | 297 |
    298 |
    299 |
    300 |
    301 |
    302 | 303 | 306 | 307 |
    308 | 309 |
    310 | 311 | 312 | 313 | Show fields in the diagram. Does not affect your field selections. 314 |
    315 | 316 | 317 | (i.e. CreatedBy, Owner) 318 |
    319 |
    320 |
    321 | 322 |
    323 | 324 |
    325 |

    ERD Content

    326 |
    327 | 331 |
    343 |
    344 |
    345 |
    346 | 347 | 348 |
    Generating..
    349 |
    350 |
    351 |
    352 |
    353 |
    354 |
    355 | 356 |
    357 |
    358 |
    359 | 360 | 361 | 362 | 363 |
    -------------------------------------------------------------------------------- /src/pages/ERDGeneratorPage.page-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31.0 4 | false 5 | false 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/staticresources/DefaultDiagram.resource: -------------------------------------------------------------------------------- 1 | {"version":3,"includeFields":true,"includeStandardUserLookup":false,"includedGroups":["Sales","Service","Marketing","Common"],"groupToObjectsToFieldsMap":{"Common":{"Contact":["Jigsaw","Languages__c","FirstName","LastName"],"Account":["Site","SLAExpirationDate__c","SLASerialNumber__c","AccountNumber","CustomerPriority__c","AnnualRevenue"]},"Service":{"CaseSolution":["CaseId","SolutionId"],"Solution":["Status","SolutionName","IsHtml","IsPublished","SolutionNumber","TimesUsed"],"CaseComment":["CommentBody","IsPublished"],"Case":["Status","IsClosed","Description","AccountId","Subject","Type","ContactId"]},"Sales":{"Opportunity":["Description","Amount","CloseDate"],"OpportunityLineItem":["ListPrice","Quantity","PricebookEntryId","TotalPrice","UnitPrice","Product2Id"]},"Marketing":{"CampaignMember":["CampaignId","Status","HasResponded","LeadId","FirstRespondedDate","ContactId"],"Lead":["Jigsaw","FirstName","LastName","Company"],"Campaign":["Name","BudgetedCost","ExpectedResponse","AmountWonOpportunities","AmountAllOpportunities","NumberOfLeads","ExpectedRevenue","ActualCost","NumberOfContacts"]}},"allSelectedObjects":["Campaign","CampaignMember","Opportunity","Contact","Case","Account","CaseComment","Solution","CaseSolution","Lead","OpportunityLineItem"]} -------------------------------------------------------------------------------- /src/staticresources/DefaultDiagram.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Private 4 | application/octet-stream 5 | 6 | -------------------------------------------------------------------------------- /src/staticresources/DefaultTemplate.resource: -------------------------------------------------------------------------------- 1 | ========== MAIN 2 | digraph G { 3 | graph [rankdir=LR,nodesep=1.0]; 4 | node [shape=plaintext,fontsize=14]; 5 | edge [arrowhead=crow]; 6 | {{content}} 7 | } 8 | ========== CLUSTER 9 | 10 | subgraph cluster_{{sequence}} { label = "{{name}}"; 11 | style="rounded, filled"; 12 | fillcolor={{color}}; 13 | node [style=filled,color=white]; 14 | 15 | {{content}} 16 | } 17 | 18 | ========== ENTITY 19 | 20 | {{name}} [label=< 21 | 22 | {{fields}} 23 |
    {{name}}
    >] 24 | 25 | ========== MD_RELATIONSHIP 26 | edge [style=solid] 27 | {{from}} -> {{to}} 28 | 29 | ========== LOOKUP_RELATIONSHIP 30 | edge [style=dashed] 31 | {{from}} -> {{to}} 32 | 33 | ========== FIELD 34 | {{name}}{{fieldType}} 35 | -------------------------------------------------------------------------------- /src/staticresources/DefaultTemplate.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Private 4 | text/plain 5 | #graphviztemplate 6 | The default ERD template 7 | 8 | -------------------------------------------------------------------------------- /src/staticresources/ERDGeneratorJS.resource: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var button = Dropbox.createChooseButton({ 4 | success: function(files) { 5 | var file = files[0].link; 6 | $c(globals.fields.fileURL).val(file); 7 | $110('#dropboxFileSelected').text(file); 8 | }, 9 | linkType: 'direct', 10 | multiselect: false, 11 | extensions: ['.gv']}); 12 | 13 | document.getElementById("pickDropboxContainer").appendChild(button); 14 | 15 | templateChanged(); 16 | 17 | $110("#oauth").on("click", function(e) { 18 | e.preventDefault(); 19 | var url = $110(this).attr("href"); 20 | window.open(url, "oauth", "width=600,height=400"); 21 | }); 22 | 23 | }()); 24 | 25 | function setDropboxToken(token) { 26 | $c(globals.fields.token).val(token); 27 | $110("#oauth").css("display", "none"); 28 | $110("#oauthComplete").css("display", "inline-block"); 29 | } 30 | 31 | function updateObjectsTab() { 32 | $c(globals.fields.fieldSelectorContainer).css("display", "none"); 33 | refreshTab3(); 34 | } 35 | 36 | function templateChanged() { 37 | adjustGeneratorButtons(); 38 | setDownloadURL(); 39 | } 40 | 41 | function setDownloadURL() { 42 | var templateSelected = $c(globals.fields.template).val(); 43 | $110("#download").attr("href", globals.templates[templateSelected]); 44 | } 45 | 46 | function adjustGeneratorButtons() { 47 | if ($c(globals.fields.template).val() == 'CalloutTemplate') { 48 | setTemplateMode('external'); 49 | } else { 50 | setTemplateMode('resource'); 51 | } 52 | } 53 | 54 | function setTemplateMode(mode) { 55 | $110.each(['#dropboxFileSelected','#pickDropboxContainer'], function(i, s) { 56 | $110(s).css('display',mode == 'resource'?'none':'inline-block'); 57 | }); 58 | } 59 | 60 | //workaround the fact that jquery doesn't like the : characters in visualforce component ids 61 | function $c(componentId) { 62 | return $110(document.getElementById(componentId)); 63 | } 64 | 65 | /*********************Javascripts to put Graphviz to web**********************/ 66 | function inspect(s) { 67 | return "
    " + s.replace(//g, ">").replace(/\"/g, """) + "
    " 68 | } 69 | 70 | function convertERDContentToMarkup(content, format, engine) { 71 | var result; 72 | try { 73 | result = Viz(content, format, engine); 74 | if (format === "svg") 75 | return result; 76 | else 77 | return inspect(result); 78 | } catch(e) { 79 | return inspect(e.toString()); 80 | } 81 | } 82 | 83 | function generateERDToWeb(content) 84 | { 85 | var erdMarkup = convertERDContentToMarkup(content,"svg"); 86 | $c(globals.fields.downloadFile).css("display", "block"); 87 | document.getElementById("diagramContainer").innerHTML = erdMarkup; 88 | } 89 | 90 | -------------------------------------------------------------------------------- /src/staticresources/ERDGeneratorJS.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Private 4 | text/javascript 5 | All the javascript for the ERD Generator page 6 | 7 | -------------------------------------------------------------------------------- /src/staticresources/VizJS.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Private 4 | text/javascript 5 | Javascript library for Graphviz 6 | 7 | -------------------------------------------------------------------------------- /src/staticresources/WorkingImage.resource: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevebuik/GraphVizForce/95095cc8d768d57838df22fb89967948f48427f1/src/staticresources/WorkingImage.resource -------------------------------------------------------------------------------- /src/staticresources/WorkingImage.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Private 4 | image/gif 5 | The animated gif displayed while waiting for an operation to complete 6 | 7 | -------------------------------------------------------------------------------- /src/tabs/ERD_Generator.tab: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | Custom19: Wrench 6 | ERDGeneratorPage 7 | 8 | --------------------------------------------------------------------------------