├── .gitignore ├── LICENSE.md ├── README.md ├── bin └── cli ├── files ├── describe │ └── .gitkeep ├── metadata │ └── .gitkeep └── tooling │ └── .gitkeep ├── index.js ├── lib ├── downloader.js ├── excelbuilder.js └── utils.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | files/**/*.json 4 | files/**/*.log 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gil Avignon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | ![version](https://img.shields.io/badge/version-1.2.15-blue) 3 | 4 | Generate data dictionary from a Salesforce Org. This tool can also generate a file that can be imported in Lucidchart to define entities and relationships. 5 | 6 | ## Getting Started 7 | 8 | Works in Unix like system. Windows is not tested. 9 | 10 | ### Installing 11 | 12 | ``` 13 | npm install -g sfdc-generate-data-dictionary 14 | ``` 15 | 16 | ## Screenshots 17 | 18 | 19 | 20 | ## Usage 21 | 22 | ### Command Line 23 | 24 | ``` 25 | $ sgd -h 26 | 27 | Usage: sgd [options] 28 | 29 | Generate data dictionary from a Salesforce Org 30 | 31 | Options: 32 | 33 | -u, --username [username] salesforce username 34 | -p, --password [password] salesforce password 35 | -l, --loginUrl [loginUrl] salesforce login URL [https://login.salesforce.com] 36 | -a, --apiVersion [apiVersion] salesforce API Version [48.0] 37 | -c, --allCustomObjects [allCustomObjects] retrieve all custom objects [true] 38 | -lc, --lucidchart [lucidchart] generate ERD file for Lucidchart [true] 39 | -s, --sobjects [sobjects] sObjects to retrieve separated with commas 40 | -D, --debug [debug] generate debug log file [false] 41 | -d, --deleteFolders [deleteFolders] delete/clean temp folders [true] 42 | -e, --excludeManagedPackage [excludeManagedPackage] exclude managed packaged [true] 43 | -ht, --hideTechFields [hideTechFields] hide tech fields [false] 44 | -tp, --techFieldPrefix [techFieldPrefix] Tech field prefix ['TECH_'] 45 | -o, --output [dir] salesforce data dictionary directory path [.] 46 | ``` 47 | 48 | #### Example 49 | ``` 50 | $ sgd -u "my.username@mydomain.com" -p "password" -l "https://test.salesforce.com" --sobjects "Account,Contact,Opportunity,Case" -c false 51 | ``` 52 | 53 | ### Module 54 | 55 | ``` 56 | var sgd = require('sfdc-generate-data-dictionary'); 57 | 58 | sgd({ 59 | 'username': '', 60 | 'password': options.password, 61 | 'loginUrl': options.loginUrl, 62 | 'projectName': '', 63 | 'allCustomObjects': true, 64 | 'debug': false, 65 | 'cleanFolders': true, 66 | 'output':'.' 67 | }, console.log); 68 | ``` 69 | 70 | ## Debugging 71 | 72 | Since **1.0.3**, you can now run the tool in debug mode to generate a file that contains information about each step during the process. 73 | Information contained in the debug files will be enriched following your feedback to have the most accurate information for debugging. 74 | 75 | Please paste the content of this file in your issues to help analysis. 76 | 77 | ### Debug files location 78 | 79 | For a local module: 80 | ``` 81 | CURRENT_DIR/node_modules/sfdc-generate-data-dictionary/files 82 | ``` 83 | 84 | Global module: 85 | - Mac: /usr/local/lib/node_modules/sfdc-generate-data-dictionary/files 86 | - Windows: %AppData%\npm\node_modules\sfdc-generate-data-dictionary\files 87 | 88 | ## Built With 89 | 90 | - [commander](https://github.com/tj/commander.js/) - The complete solution for node.js command-line interfaces, inspired by Ruby's commander. 91 | - [bytes](https://github.com/visionmedia/bytes.js) - Utility to parse a string bytes to bytes and vice-versa. 92 | - [excel4node](https://github.com/amekkawi/excel4node) - Node module to allow for easy Excel file creation. 93 | - [jsforce](https://github.com/jsforce/jsforce) - Salesforce API Library for JavaScript applications (both on Node.js and web browser) 94 | 95 | ## Versioning 96 | 97 | [SemVer](http://semver.org/) is used for versioning. 98 | 99 | ## Authors 100 | 101 | - **Gil Avignon** - _Initial work_ - [gavignon](https://github.com/gavignon) 102 | 103 | ## License 104 | 105 | This project is licensed under the MIT License - see the file for details 106 | -------------------------------------------------------------------------------- /bin/cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const program = require('commander'); 5 | const orchestrator = require('../index.js'); 6 | const pjson = require('../package.json'); 7 | 8 | program 9 | .description(pjson.description) 10 | .version(pjson.version) 11 | .option('-u, --username [username]', 'salesforce username') 12 | .option('-p, --password [password]', 'salesforce password') 13 | .option('-l, --loginUrl [loginUrl]', 'salesforce login URL [https://login.salesforce.com]', 'https://login.salesforce.com') 14 | .option('-a, --apiVersion [apiVersion]', 'salesforce API Version [48.0]', '48.0') 15 | .option('-c, --allCustomObjects [allCustomObjects]', 'retrieve all custom objects [true]', true) 16 | .option('-lc, --lucidchart [lucidchart]', 'generate ERD file for Lucidchart [true]', true) 17 | .option('-s, --sobjects [sobjects]', 'sObjects to retrieve separated with commas') 18 | .option('-D, --debug [debug]', 'generate debug log file [false]', false) 19 | .option('-e, --excludeManagedPackage [excludeManagedPackage]', 'exclude managed packaged [true]', true) 20 | .option('-d, --deleteFolders [deleteFolders]', 'delete/clean temp folders [true]', true) 21 | .option('-ht, --hideTechFields [hideTechFields]', 'hide tech fields', false) 22 | .option('-tp, --techFieldPrefix [techFieldPrefix]', 'Tech field prefix', 'TECH_') 23 | .option('-t, --outputTime [outputTime]', 'Display Hours in the file name', false) 24 | .option('-o, --output [dir]', 'salesforce data dictionary directory path [.]', '.') 25 | .parse(process.argv); 26 | 27 | orchestrator(program, console.log) 28 | .catch(function(err){ 29 | throw err; 30 | }); 31 | -------------------------------------------------------------------------------- /files/describe/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavignon/sfdc-generate-data-dictionary/bd7d40a957aaa1b75b8140cd7dde318aa5d4e891/files/describe/.gitkeep -------------------------------------------------------------------------------- /files/metadata/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavignon/sfdc-generate-data-dictionary/bd7d40a957aaa1b75b8140cd7dde318aa5d4e891/files/metadata/.gitkeep -------------------------------------------------------------------------------- /files/tooling/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavignon/sfdc-generate-data-dictionary/bd7d40a957aaa1b75b8140cd7dde318aa5d4e891/files/tooling/.gitkeep -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const jsforce = require('jsforce'); 3 | const Downloader = require('./lib/downloader.js'); 4 | const ExcelBuilder = require('./lib/excelbuilder.js'); 5 | const Utils = require('./lib/utils.js'); 6 | 7 | module.exports = (config, logger) => { 8 | 9 | 10 | 11 | // Check all mandatory config options 12 | if (typeof config.username === 'undefined' || config.username === null || 13 | typeof config.password === 'undefined' || config.password === null) { 14 | throw new Error('Not enough config options'); 15 | } 16 | 17 | // Set default values 18 | if (typeof config.loginUrl === 'undefined' || config.loginUrl === null) { 19 | config.loginUrl = 'https://login.salesforce.com'; 20 | } 21 | if (typeof config.apiVersion === 'undefined' || config.apiVersion === null) { 22 | config.apiVersion = '48.0'; 23 | } 24 | if (typeof config.output === 'undefined' || config.output === null) { 25 | config.output = '.'; 26 | } 27 | if (typeof config.debug === 'undefined' || config.debug === null) { 28 | config.debug = false; 29 | } 30 | config.debug = (config.debug === "true" || config.debug === true); 31 | 32 | if (typeof config.excludeManagedPackage === 'undefined' || config.excludeManagedPackage === null) { 33 | config.excludeManagedPackage = true; 34 | } 35 | config.excludeManagedPackage = (config.excludeManagedPackage === "true" || config.excludeManagedPackage === true); 36 | 37 | if (typeof config.projectName === 'undefined' || config.projectName === null) { 38 | config.projectName = 'PROJECT'; 39 | } 40 | if (typeof config.outputTime === 'undefined' || config.outputTime === null) { 41 | config.outputTime = false; 42 | } 43 | if (typeof config.allCustomObjects === 'undefined' || config.allCustomObjects === null) { 44 | config.allCustomObjects = true; 45 | } 46 | config.allCustomObjects = (config.allCustomObjects === "true" || config.allCustomObjects === true); 47 | 48 | if (typeof config.lucidchart === 'undefined' || config.lucidchart === null) { 49 | config.lucidchart = true; 50 | } 51 | config.lucidchart = (config.lucidchart === "true" || config.lucidchart === true); 52 | 53 | if (typeof config.sobjects === 'undefined' || config.sobjects === null) { 54 | config.objects = [ 55 | 'Account', 56 | 'Contact', 57 | 'User' 58 | ]; 59 | } else { 60 | // If an array is passed to the module 61 | if (Array.isArray(config.sobjects)) { 62 | config.objects = config.sobjects; 63 | } else { 64 | // Check and parse standObjects string for command-line 65 | try { 66 | config.objects = config.sobjects.split(','); 67 | } catch (e) { 68 | let errorMessage = 'Unable to parse sobjects parameter'; 69 | if (config.debug) 70 | errorMessage += ' : ' + e; 71 | throw new Error(errorMessage); 72 | } 73 | } 74 | } 75 | 76 | 77 | if (typeof config.techFieldPrefix === 'undefined' || config.techFieldPrefix === null) { 78 | config.techFieldPrefix = 'TECH_'; 79 | } 80 | if (typeof config.hideTechFields === 'undefined' || config.hideTechFields === null) { 81 | config.hideTechFields = false; 82 | } 83 | if (typeof config.columns === 'undefined' || config.columns === null) { 84 | config.columns = { 85 | 'ReadOnly': 5, 86 | 'Mandatory': 3, 87 | 'Name': 25, 88 | 'Description': 90, 89 | 'Helptext': 90, 90 | 'APIName': 25, 91 | 'Type': 27, 92 | 'Values': 45 93 | }; 94 | } 95 | 96 | var utils = new Utils(); 97 | 98 | // Clean folders that contain API files 99 | if (config.cleanFolders) { 100 | const statusRmDescribe = utils.rmDir(__dirname + '/files/describe', '.json', false); 101 | const statusRmMetadata = utils.rmDir(__dirname + '/files/metadata', '.json', false); 102 | logger('File folders cleaned'); 103 | } 104 | 105 | // Main promise 106 | const promise = new Promise((resolve, reject) => { 107 | 108 | const conn = new jsforce.Connection({ 109 | loginUrl: config.loginUrl, 110 | version: config.apiVersion 111 | }); 112 | 113 | // Salesforce connection 114 | conn.login(config.username, config.password).then(result => { 115 | logger('Connected as ' + config.username); 116 | if (config.debug) { 117 | utils.log('Connected as ' + config.username, config); 118 | } 119 | 120 | if (config.allCustomObjects) { 121 | conn.describeGlobal().then(res => { 122 | for (let i = 0; i < res.sobjects.length; i++) { 123 | let object = res.sobjects[i]; 124 | if (config.objects === undefined) 125 | config.objects = []; 126 | 127 | // If the sObject is a real custom object 128 | if (object.custom && (object.name.indexOf('__c') !== -1)) { 129 | if (config.debug) 130 | utils.log('# excludeManagedPackage (' + config.excludeManagedPackage + '): ' + object.name, config); 131 | 132 | if (config.excludeManagedPackage) { 133 | if ((object.name.split('__').length - 1 < 2)) 134 | config.objects.push(object.name); 135 | } else { 136 | config.objects.push(object.name); 137 | } 138 | } 139 | } 140 | 141 | if (config.debug) 142 | utils.log(JSON.stringify(config.objects), config); 143 | 144 | const downloader = new Downloader(config, logger, conn); 145 | const builder = new ExcelBuilder(config, logger); 146 | 147 | // Download metadata files 148 | downloader.execute().then(result => { 149 | logger(result + ' downloaded'); 150 | // Generate the excel file 151 | builder.generate().then(result => { 152 | resolve(); 153 | }); 154 | }) 155 | }); 156 | } else { 157 | if (config.objects.length > 0) { 158 | const downloader = new Downloader(config, logger, conn); 159 | const builder = new ExcelBuilder(config, logger); 160 | 161 | // Download metadata files 162 | downloader.execute().then(result => { 163 | logger(result + ' downloaded'); 164 | // Generate the excel file 165 | return builder.generate(); 166 | 167 | }).then(result => { 168 | resolve(); 169 | }); 170 | 171 | } 172 | } 173 | }).catch(reject); 174 | }); 175 | return promise; 176 | }; 177 | -------------------------------------------------------------------------------- /lib/downloader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const bytes = require('bytes'); 4 | const Utils = require('./utils.js'); 5 | 6 | const FILE_DIR = '../files'; 7 | 8 | module.exports = class Downloader { 9 | constructor(config, logger, conn) { 10 | this.config = config; 11 | this.logger = logger; 12 | this.conn = conn; 13 | this.utils = new Utils(logger); 14 | } 15 | 16 | downloadDescribe(sObject) { 17 | const self = this; 18 | return new Promise((resolve, reject) => { 19 | self.conn.sobject(sObject).describe().then(meta => { 20 | const filePath = path.join(__dirname, FILE_DIR, '/describe/', sObject + '.json'); 21 | fs.writeFileSync(filePath, JSON.stringify(meta.fields), 'utf-8'); 22 | const stats = fs.statSync(filePath); 23 | 24 | resolve(stats.size); 25 | }).catch(function(err) { 26 | reject(sObject + ': ' + err); 27 | if (self.config.debug) { 28 | self.utils.log(err, self.config); 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | downloadMetadata(sobjectList) { 35 | const self = this; 36 | return new Promise((resolve, reject) => { 37 | self.conn.metadata.read('CustomObject', sobjectList).then(metadata => { 38 | let filePath = ''; 39 | 40 | 41 | 42 | if (sobjectList.length === 1) { 43 | let fields = metadata.fields; 44 | fields.sort(self.utils.sortByProperty('fullName')); 45 | filePath = path.join(__dirname, FILE_DIR, '/metadata/', metadata.fullName + '.json'); 46 | fs.writeFileSync(filePath, JSON.stringify(metadata), 'utf-8'); 47 | } else { 48 | for (let i = 0; i < metadata.length; i++) { 49 | 50 | let fields = metadata[i].fields; 51 | if ((!Array.isArray(fields) || (Array.isArray(fields) && (fields !== undefined || fields.length > 0)))) { 52 | // Manage single object or an object array 53 | if(fields != null && !Array.isArray(fields)){ 54 | let fieldsArray = new Array(); 55 | fieldsArray.push(fields); 56 | fields = fieldsArray; 57 | metadata[i].fields = fields; 58 | } 59 | 60 | filePath = path.join(__dirname, FILE_DIR, '/metadata/', metadata[i].fullName + '.json'); 61 | fs.writeFileSync(filePath, JSON.stringify(metadata[i]), 'utf-8'); 62 | } else { 63 | self.config.objects.splice(self.config.objects.indexOf(metadata[i]), 1); 64 | } 65 | } 66 | } 67 | const stats = fs.statSync(filePath); 68 | 69 | resolve(stats.size); 70 | }).catch(function(err) { 71 | reject(err); 72 | if (self.config.debug) { 73 | self.utils.log(err, self.config); 74 | } 75 | }); 76 | }); 77 | } 78 | 79 | execute() { 80 | const promise = new Promise((resolve, reject) => { 81 | const self = this; 82 | 83 | this.logger('Downloading...'); 84 | 85 | let downloadArray = new Array(); 86 | 87 | for (let object of self.config.objects) { 88 | downloadArray.push(self.downloadDescribe(object)); 89 | } 90 | 91 | let loop = ~~(self.config.objects.length / 10); 92 | if (self.config.objects.length % 10 > 0) 93 | loop++; 94 | 95 | let j = 0; 96 | for (let i = 0; i < loop; i++) { 97 | let objectList = self.config.objects.slice(j, j + 10); 98 | j += 10; 99 | downloadArray.push(self.downloadMetadata(objectList)); 100 | } 101 | 102 | Promise.all( 103 | downloadArray 104 | ).then(results => { 105 | let total = 0; 106 | for (let fileSize of results) { 107 | total += fileSize; 108 | } 109 | resolve(bytes.format(total, { 110 | decimalPlaces: 2 111 | })); 112 | }).catch(err => { 113 | if (self.config.debug) { 114 | self.utils.log(err, self.config); 115 | } 116 | self.logger(err); 117 | }); 118 | }); 119 | return promise; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/excelbuilder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const excel = require('excel4node'); 3 | const path = require('path'); 4 | const Utils = require('./utils.js'); 5 | 6 | const FILE_DIR = '../files'; 7 | const MAX_PICKLIST_VALUES = 2; 8 | 9 | // Styles 10 | var workbook = new excel.Workbook(); 11 | var startGeneration; 12 | 13 | var global = workbook.createStyle({ 14 | font: { 15 | size: 12 16 | }, 17 | alignment: { 18 | wrapText: true, 19 | vertical: 'center', 20 | }, 21 | border: { 22 | left: { 23 | style: 'thin', 24 | color: 'b8b6b8' 25 | }, 26 | right: { 27 | style: 'thin', 28 | color: 'b8b6b8' 29 | }, 30 | top: { 31 | style: 'thin', 32 | color: 'b8b6b8' 33 | }, 34 | bottom: { 35 | style: 'thin', 36 | color: 'b8b6b8' 37 | } 38 | } 39 | }); 40 | 41 | var header = workbook.createStyle({ 42 | font: { 43 | bold: true, 44 | color: 'FFFFFF' 45 | }, 46 | alignment: { 47 | horizontal: 'center' 48 | }, 49 | fill: { 50 | type: 'pattern', 51 | patternType: 'solid', 52 | fgColor: '019cdd' 53 | } 54 | }); 55 | 56 | var subHeader = workbook.createStyle({ 57 | font: { 58 | bold: true 59 | }, 60 | fill: { 61 | type: 'pattern', 62 | patternType: 'solid', 63 | fgColor: 'F5F4F2' // HTML style hex value. optional. defaults to black 64 | } 65 | }); 66 | 67 | var category = workbook.createStyle({ 68 | font: { 69 | // bold: true, 70 | color: '60809f' 71 | }, 72 | fill: { 73 | type: 'pattern', 74 | patternType: 'solid', 75 | fgColor: 'dbeaf7' 76 | } 77 | }); 78 | 79 | var validationCategory = workbook.createStyle({ 80 | font: { 81 | // bold: true, 82 | color: '703026' 83 | }, 84 | fill: { 85 | type: 'pattern', 86 | patternType: 'solid', 87 | fgColor: 'ffa293' 88 | } 89 | }); 90 | 91 | var indentLeft = workbook.createStyle({ 92 | alignment: { 93 | indent: 1 94 | } 95 | }); 96 | 97 | var centerAlign = workbook.createStyle({ 98 | alignment: { 99 | horizontal: 'center' 100 | } 101 | }); 102 | var bold = workbook.createStyle({ 103 | font: { 104 | bold: true 105 | } 106 | }); 107 | var italic = workbook.createStyle({ 108 | font: { 109 | italics: true 110 | } 111 | }); 112 | var redColor = workbook.createStyle({ 113 | font: { 114 | color: 'FF0000' 115 | } 116 | }); 117 | 118 | var rowColor = workbook.createStyle({ 119 | fill: { 120 | type: 'pattern', 121 | patternType: 'solid', 122 | fgColor: 'ffffff' 123 | } 124 | }); 125 | 126 | var alternateRowColor = workbook.createStyle({ 127 | fill: { 128 | type: 'pattern', 129 | patternType: 'solid', 130 | fgColor: 'f2f1f3' 131 | } 132 | }); 133 | 134 | module.exports = class Downloader { 135 | constructor(config, logger) { 136 | this.config = config; 137 | this.logger = logger; 138 | this.utils = new Utils(); 139 | } 140 | 141 | createHeader(worksheet) { 142 | 143 | var columns = this.config.columns; 144 | var columnsKeys = Object.keys(this.config.columns); 145 | 146 | // Global sizes 147 | worksheet.row(1).setHeight(40); 148 | worksheet.row(2).setHeight(20); 149 | 150 | if (columnsKeys.indexOf('ReadOnly') > -1) 151 | worksheet.column(columnsKeys.indexOf('ReadOnly') + 1).setWidth(columns.ReadOnly); 152 | if (columnsKeys.indexOf('Mandatory') > -1) 153 | worksheet.column(columnsKeys.indexOf('Mandatory') + 1).setWidth(columns.Mandatory); 154 | if (columnsKeys.indexOf('Name') > -1) 155 | worksheet.column(columnsKeys.indexOf('Name') + 1).setWidth(columns.Name); 156 | if (columnsKeys.indexOf('Description') > -1) 157 | worksheet.column(columnsKeys.indexOf('Description') + 1).setWidth(columns.Description); 158 | if (columnsKeys.indexOf('Helptext') > -1) 159 | worksheet.column(columnsKeys.indexOf('Helptext') + 1).setWidth(columns.Helptext); 160 | if (columnsKeys.indexOf('APIName') > -1) 161 | worksheet.column(columnsKeys.indexOf('APIName') + 1).setWidth(columns.APIName); 162 | if (columnsKeys.indexOf('Type') > -1) 163 | worksheet.column(columnsKeys.indexOf('Type') + 1).setWidth(columns.Type); 164 | if (columnsKeys.indexOf('Values') > -1) 165 | worksheet.column(columnsKeys.indexOf('Values') + 1).setWidth(columns.Values); 166 | 167 | // Build header and subheader 168 | worksheet.cell(1, 1, 1, columnsKeys.length, true).string('SALESFORCE').style(global).style(header); 169 | 170 | if (columnsKeys.indexOf('ReadOnly') > -1) 171 | worksheet.cell(2, columnsKeys.indexOf('ReadOnly') + 1).string('R/O').style(global).style(subHeader).style(centerAlign); 172 | if (columnsKeys.indexOf('Mandatory') > -1) 173 | worksheet.cell(2, columnsKeys.indexOf('Mandatory') + 1).string('M').style(global).style(subHeader).style(centerAlign); 174 | if (columnsKeys.indexOf('Name') > -1) 175 | worksheet.cell(2, columnsKeys.indexOf('Name') + 1).string('Field Name').style(global).style(subHeader).style(indentLeft); 176 | if (columnsKeys.indexOf('Description') > -1) 177 | worksheet.cell(2, columnsKeys.indexOf('Description') + 1).string('Description').style(global).style(subHeader).style(indentLeft); 178 | if (columnsKeys.indexOf('Helptext') > -1) 179 | worksheet.cell(2, columnsKeys.indexOf('Helptext') + 1).string('Helptext').style(global).style(subHeader).style(indentLeft); 180 | if (columnsKeys.indexOf('APIName') > -1) 181 | worksheet.cell(2, columnsKeys.indexOf('APIName') + 1).string('API Name').style(global).style(subHeader).style(indentLeft); 182 | if (columnsKeys.indexOf('Type') > -1) 183 | worksheet.cell(2, columnsKeys.indexOf('Type') + 1).string('Type').style(global).style(subHeader).style(centerAlign); 184 | if (columnsKeys.indexOf('Values') > -1) 185 | worksheet.cell(2, columnsKeys.indexOf('Values') + 1).string('Values / Formula').style(global).style(subHeader).style(indentLeft); 186 | 187 | return 3; 188 | } 189 | 190 | mapFields(fields) { 191 | var fieldMap = {}; 192 | 193 | for (var i = 0; i < fields.length; i++) { 194 | var field = fields[i]; 195 | fieldMap[field.fullName] = field; 196 | } 197 | 198 | return fieldMap; 199 | } 200 | 201 | writeFields(worksheet, fields, line, validationRules) { 202 | 203 | 204 | var columns = this.config.columns; 205 | var columnsKeys = Object.keys(this.config.columns); 206 | 207 | var indexRow = 1; 208 | 209 | // Foreach field 210 | for (var j = 0; j < fields.length; j++) { 211 | var field = fields[j]; 212 | 213 | if (!(this.config.hideTechFields && field.name.startsWith(this.config.techFieldPrefix))) { 214 | 215 | var isCustom = field.custom; 216 | 217 | if (!isCustom && j == 0) { 218 | worksheet.cell(line, 1, line, columnsKeys.length, true).string('Standard Fields').style(global).style(category).style(indentLeft); 219 | // Row height 220 | worksheet.row(line).setHeight(25); 221 | line++; 222 | indexRow = 1; 223 | } 224 | 225 | var rowStyle = rowColor; 226 | if (indexRow % 2 == 0) { 227 | rowStyle = alternateRowColor; 228 | } 229 | 230 | 231 | if (columnsKeys.indexOf('ReadOnly') > -1) 232 | worksheet.cell(line, columnsKeys.indexOf('ReadOnly') + 1).string(!field.updateable ? "✓" : '☐').style(global).style(centerAlign).style(rowStyle); 233 | if (columnsKeys.indexOf('Mandatory') > -1) 234 | worksheet.cell(line, columnsKeys.indexOf('Mandatory') + 1).string(!field.nillable && field.updateable && field.type != 'boolean' ? "*" : '').style(global).style(centerAlign).style(rowStyle).style(redColor); 235 | if (columnsKeys.indexOf('Name') > -1) 236 | worksheet.cell(line, columnsKeys.indexOf('Name') + 1).string(field.label != null ? field.label : field.name).style(global).style(bold).style(rowStyle).style(indentLeft); 237 | if (columnsKeys.indexOf('Description') > -1) 238 | worksheet.cell(line, columnsKeys.indexOf('Description') + 1).string(field.description != null ? field.description : '').style(global).style(rowStyle).style(indentLeft); 239 | if (columnsKeys.indexOf('Helptext') > -1) 240 | worksheet.cell(line, columnsKeys.indexOf('Helptext') + 1).string(field.inlineHelpText != null ? field.inlineHelpText : '').style(global).style(rowStyle).style(indentLeft); 241 | if (columnsKeys.indexOf('APIName') > -1) 242 | worksheet.cell(line, columnsKeys.indexOf('APIName') + 1).string(field.name).style(global).style(rowStyle).style(indentLeft); 243 | 244 | // tooling 245 | // worksheet.cell(line, columnsKeys.indexOf('APIName') + 4).string(field.LastModifiedDate != null ? field.LastModifiedDate : '').style(global).style(rowStyle).style(indentLeft); 246 | 247 | // Type property 248 | var type = this.utils.capitalize(field.type); 249 | 250 | if (type == 'Int' || type == 'Double') { 251 | type = 'Number'; 252 | } 253 | if (type == 'Number' || type == 'Currency') { 254 | var precision = parseInt(field.precision); 255 | var scale = parseInt(field.scale); 256 | var finalPrecision = precision - scale; 257 | 258 | type = type + '(' + finalPrecision + ',' + field.scale + ')'; 259 | } 260 | 261 | if (type == 'Boolean') { 262 | type = 'Checkbox'; 263 | } 264 | 265 | if (type == 'Reference' && field.referenceTo != null) { 266 | type = 'Lookup(' + field.referenceTo + ')'; 267 | } 268 | if (type == 'MasterDetail') { 269 | type = 'Master-Detail(' + field.referenceTo + ')'; 270 | } 271 | if ((type == 'Text' || type == 'Textarea' || type == 'String') && field.length != null) { 272 | type = 'Text(' + field.length + ')'; 273 | } 274 | 275 | if (field.calculatedFormula != null) { 276 | type = 'Formula(' + field.type + ')'; 277 | } 278 | 279 | if (!field.nillable) { 280 | type += ' (Unique)'; 281 | } 282 | if (field.externalId) { 283 | type += '(External ID)'; 284 | } 285 | 286 | if (columnsKeys.indexOf('Type') > -1) 287 | worksheet.cell(line, columnsKeys.indexOf('Type') + 1).string(type).style(centerAlign).style(global).style(italic).style(rowStyle).style(indentLeft); 288 | 289 | 290 | // Values property 291 | var value = ''; 292 | 293 | if (type == 'Picklist' || type == 'MultiselectPicklist') { 294 | if (field.globalPicklist != null) { 295 | value = 'globalPicklist(' + field.globalPicklist + ')'; 296 | } else { 297 | var valuesArray = field.picklistValues; 298 | var k = 0; 299 | while (k < valuesArray.length && k < MAX_PICKLIST_VALUES) { 300 | value += valuesArray[k].value + '\n'; 301 | k++; 302 | } 303 | if (valuesArray.length > MAX_PICKLIST_VALUES * 2) { 304 | value += '...\n'; 305 | } 306 | if (valuesArray.length - MAX_PICKLIST_VALUES >= MAX_PICKLIST_VALUES) { 307 | k = valuesArray.length - 1 308 | while (k >= valuesArray.length - MAX_PICKLIST_VALUES) { 309 | value += valuesArray[k].value + '\n'; 310 | k--; 311 | } 312 | } 313 | if (valuesArray.length > MAX_PICKLIST_VALUES * 2) { 314 | value += '(Total: ' + valuesArray.length + ' values)'; 315 | } 316 | } 317 | } 318 | 319 | if (field.calculatedFormula != null) { 320 | value = field.calculatedFormula; 321 | } 322 | 323 | if (columnsKeys.indexOf('Values') > -1) 324 | worksheet.cell(line, columnsKeys.indexOf('Values') + 1).string(value).style(global).style(rowStyle).style(indentLeft); 325 | 326 | if (((!field.label.length < 24) || (!field.name.length < 24)) && !value.includes('\n')) 327 | worksheet.row(line).setHeight(25); 328 | line++; 329 | indexRow++; 330 | 331 | if (!isCustom && j + 1 < fields.length && fields[j + 1].custom) { 332 | worksheet.cell(line, 1, line, columnsKeys.length, true).string('Custom Fields').style(global).style(category).style(indentLeft); 333 | // Row height 334 | worksheet.row(line).setHeight(25); 335 | line++; 336 | indexRow = 1; 337 | } 338 | } 339 | } 340 | 341 | if (validationRules !== undefined) { 342 | 343 | worksheet.cell(line, 1, line, columnsKeys.length, true).string('Validation Rules').style(global).style(validationCategory).style(indentLeft); 344 | // Row height 345 | worksheet.row(line).setHeight(25); 346 | line++; 347 | 348 | worksheet.cell(line, 1, line, 2, true).string('Active').style(global).style(rowStyle).style(subHeader).style(centerAlign); 349 | worksheet.cell(line, 3).string('Name').style(global).style(rowStyle).style(subHeader).style(indentLeft); 350 | worksheet.cell(line, 4).string('Description').style(global).style(rowStyle).style(subHeader).style(indentLeft); 351 | worksheet.cell(line, 5).string('Error display field').style(global).style(rowStyle).style(subHeader).style(centerAlign); 352 | worksheet.cell(line, 6).string('Error message').style(global).style(rowStyle).style(subHeader).style(indentLeft); 353 | if (columnsKeys.indexOf('Helptext') > -1){ 354 | worksheet.cell(line, 7, line, 8, true).string('Condition formula').style(global).style(rowStyle).style(subHeader).style(indentLeft); 355 | }else{ 356 | worksheet.cell(line, 7).string('Condition formula').style(global).style(rowStyle).style(subHeader).style(indentLeft); 357 | } 358 | worksheet.row(line).setHeight(20); 359 | 360 | line++; 361 | indexRow = 1; 362 | 363 | if (Array.isArray(validationRules)) { 364 | for (var k = 0; k < validationRules.length; k++) { 365 | rowStyle = rowColor; 366 | if (indexRow % 2 == 0) { 367 | rowStyle = alternateRowColor; 368 | } 369 | 370 | worksheet.cell(line, 1, line, 2, true).string(validationRules[k].active === "true" ? "✓" : '☐').style(global).style(rowStyle).style(centerAlign); 371 | worksheet.cell(line, 3).string(validationRules[k].fullName != null ? validationRules[k].fullName : '').style(global).style(rowStyle).style(indentLeft); 372 | worksheet.cell(line, 4).string(validationRules[k].description != null ? validationRules[k].description : '').style(global).style(rowStyle).style(indentLeft); 373 | worksheet.cell(line, 5).string(validationRules[k].errorDisplayField != null ? validationRules[k].errorDisplayField : '').style(global).style(rowStyle).style(centerAlign); 374 | worksheet.cell(line, 6).string(validationRules[k].errorMessage != null ? validationRules[k].errorMessage : '').style(global).style(rowStyle).style(indentLeft); 375 | if (columnsKeys.indexOf('Helptext') > -1){ 376 | worksheet.cell(line, 7, line, 8, true).string(validationRules[k].errorConditionFormula != null ? validationRules[k].errorConditionFormula : '').style(global).style(rowStyle).style(indentLeft); 377 | }else{ 378 | worksheet.cell(line, 7).string(validationRules[k].errorConditionFormula != null ? validationRules[k].errorConditionFormula : '').style(global).style(rowStyle).style(indentLeft); 379 | } 380 | 381 | line++; 382 | indexRow++; 383 | } 384 | } else { 385 | rowStyle = rowColor; 386 | if (indexRow % 2 == 0) { 387 | rowStyle = alternateRowColor; 388 | } 389 | 390 | worksheet.cell(line, 1, line, 2, true).string(validationRules.active === "true" ? "✓" : '☐').style(global).style(rowStyle).style(centerAlign); 391 | worksheet.cell(line, 3).string(validationRules.fullName != null ? validationRules.fullName : '').style(global).style(rowStyle).style(indentLeft); 392 | worksheet.cell(line, 4).string(validationRules.description != null ? validationRules.description : '').style(global).style(rowStyle).style(indentLeft); 393 | worksheet.cell(line, 5).string(validationRules.errorDisplayField != null ? validationRules.errorDisplayField : '').style(global).style(rowStyle).style(centerAlign); 394 | worksheet.cell(line, 6).string(validationRules.errorMessage != null ? validationRules.errorMessage : '').style(global).style(rowStyle).style(indentLeft); 395 | worksheet.cell(line, 7).string(validationRules.errorConditionFormula != null ? validationRules.errorConditionFormula : '').style(global).style(rowStyle).style(indentLeft); 396 | 397 | line++; 398 | indexRow++; 399 | } 400 | } 401 | } 402 | 403 | generateChart(objectName, fields){ 404 | var chart = '' + '\n' + '
'; 405 | var cpt = 0; 406 | 407 | // Foreach field 408 | for (var j = 0; j < fields.length; j++) { 409 | var field = fields[j]; 410 | 411 | // Type property 412 | var type = this.utils.capitalize(field.type); 413 | var add = false; 414 | var attribute = null; 415 | var fieldLength = field.length != null ? field.length : ''; 416 | var relationObject = ''; 417 | var attributeKey = ''; 418 | var attributeType = ''; 419 | 420 | if (type == 'Reference' && field.referenceTo != null) { 421 | add = true; 422 | attributeKey = 'FOREIGN KEY'; 423 | attributeType = 'LOOKUP'; 424 | relationObject = field.referenceTo; 425 | } 426 | if (type == 'MasterDetail') { 427 | add = true; 428 | attributeKey = 'FOREIGN KEY'; 429 | attributeType = 'MASTER DETAIL'; 430 | relationObject = field.referenceTo; 431 | } 432 | if (type === 'Id'){ 433 | add = true; 434 | attributeKey = 'PRIMARY KEY'; 435 | attributeType = 'ID'; 436 | } 437 | 438 | if(add){ 439 | var fieldLabel = field.label != null ? field.label : field.name; 440 | var fieldName = field.name; 441 | 442 | if(type === 'Id'){ 443 | chart += 'postgresql;ELSA;Salesforce;"' + objectName + ' (' + objectName + ')";"' + objectName + ' ID (' + fieldName + ')";' + cpt + ';"' + attributeType + '";' + fieldLength + ';"' + attributeKey + '";;' + '\n'; 444 | }else{ 445 | chart += 'postgresql;ELSA;Salesforce;"' + objectName + ' (' + objectName + ')";"' + fieldLabel + ' (' + fieldName + ')";' + cpt + ';"' + attributeType + '";' + fieldLength + ';"' + attributeKey + '";"Salesforce";"' + relationObject + ' (' + relationObject + ')";"' + relationObject + ' ID (Id)"' + '\n'; 446 | } 447 | 448 | cpt++; 449 | } 450 | 451 | } 452 | chart += '
' + '\n' + '' 453 | return chart; 454 | } 455 | 456 | generate() { 457 | const promise = new Promise((resolve, reject) => { 458 | this.logger('Generating...'); 459 | 460 | let sObjects = this.config.objects; 461 | var chart = ''; 462 | 463 | for (var i = 0; i < sObjects.length; i++) { 464 | var cur = i + 1; 465 | 466 | var worksheet = workbook.addWorksheet(sObjects[i]); 467 | var line = this.createHeader(worksheet); 468 | var describePath = path.join(__dirname, FILE_DIR, '/describe/' + sObjects[i] + '.json'); 469 | var metadataPath = path.join(__dirname, FILE_DIR, '/metadata/' + sObjects[i] + '.json'); 470 | 471 | if (fs.existsSync(describePath)) { 472 | var currentObjectFieldsDescribe = JSON.parse(fs.readFileSync(describePath)); 473 | 474 | if (fs.existsSync(metadataPath)) { 475 | var currentObjectFieldsMetadata = JSON.parse(fs.readFileSync(metadataPath)); 476 | if(currentObjectFieldsMetadata.fields != null){ 477 | var fieldsMap = this.mapFields(currentObjectFieldsMetadata.fields); 478 | } 479 | } 480 | 481 | for (var j = 0; j < currentObjectFieldsDescribe.length; j++) { 482 | var field = currentObjectFieldsDescribe[j]; 483 | var fieldName = currentObjectFieldsDescribe[j].name; 484 | 485 | if (fieldsMap != null && fieldsMap[fieldName] != null) { 486 | var correspondingField = fieldsMap[fieldName]; 487 | if (correspondingField.description != null) 488 | currentObjectFieldsDescribe[j].description = correspondingField.description; 489 | 490 | if (correspondingField.type === 'MasterDetail') 491 | currentObjectFieldsDescribe[j].type = correspondingField.type; 492 | } 493 | 494 | } 495 | } 496 | 497 | currentObjectFieldsDescribe.sort(this.utils.sortByTwoProperty('custom', 'name')); 498 | 499 | if (this.config.debug) { 500 | this.utils.log('#' + sObjects[i] + '\n#Validation RULES ' + JSON.stringify(currentObjectFieldsMetadata.validationRules), this.config); 501 | } 502 | 503 | this.writeFields(worksheet, currentObjectFieldsDescribe, line, currentObjectFieldsMetadata.validationRules); 504 | if(this.config.lucidchart) 505 | chart += this.generateChart(sObjects[i], currentObjectFieldsDescribe); 506 | } 507 | 508 | if(this.config.lucidchart){ 509 | // Generate chart file (Lucidchart) 510 | this.logger('Saving lucidchart file...'); 511 | const filePath = path.join(this.config.output, 'lucidchart.txt'); 512 | fs.writeFileSync(filePath, chart, 'utf-8'); 513 | this.logger('Lucidchart.txt file successfully saved!'); 514 | } 515 | 516 | // Generate output Excel file 517 | var currentDate = new Date(Date.now()); 518 | var currentDateString = currentDate.toISOString(); 519 | if(this.config.outputTime){ 520 | currentDateString = currentDateString.replace('T', '_').replace('Z', '').replace(/:/g,'_').replace('.','_'); 521 | }else{ 522 | currentDateString = currentDateString.substring(0, currentDateString.indexOf('T')); 523 | } 524 | var fileName = this.config.projectName + '_Data_Dictionary_' + currentDateString + '.xlsx' 525 | var outputFile = path.join(this.config.output, fileName); 526 | this.logger('Saving ' + fileName + '...'); 527 | workbook.write(outputFile); 528 | this.logger(fileName + ' successfully saved!'); 529 | 530 | resolve(); 531 | }); 532 | return promise; 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = class Utils { 5 | constructor(logger) { 6 | this.logger = logger; 7 | } 8 | 9 | log(message, config){ 10 | var date = new Date(); 11 | var currentDay = date.getDay(); 12 | var currentMonth = date.getMonth(); 13 | var currentYear = date.getFullYear(); 14 | var currentHour = date.getHours(); 15 | if(currentHour < 10) 16 | currentHour = '0' + currentHour.toString(); 17 | var currentMinutes = date.getMinutes(); 18 | if(currentMinutes < 10) 19 | currentMinutes = '0' + currentMinutes.toString(); 20 | var currentSeconds = date.getSeconds(); 21 | if(currentSeconds < 10) 22 | currentSeconds = '0' + currentSeconds.toString(); 23 | var timestamp = '[' + currentHour + ':' + currentMinutes + ':' + currentSeconds + '] '; 24 | 25 | var logMessage = timestamp + message + '\n'; 26 | if(config.debug) 27 | fs.appendFileSync(path.join(__dirname, '../files/', date.toLocaleDateString().replace(/\//g,'') + '_debug.log'), logMessage, 'utf-8'); 28 | } 29 | 30 | sortByProperty(property) { 31 | return function(a, b) { 32 | var sortStatus = 0; 33 | if (a[property] < b[property]) { 34 | sortStatus = -1; 35 | } else if (a[property] > b[property]) { 36 | sortStatus = 1; 37 | } 38 | 39 | return sortStatus; 40 | }; 41 | } 42 | 43 | sortByTwoProperty(prop1, prop2) { 44 | 'use strict'; 45 | return function(a, b) { 46 | if (a[prop1] === undefined) { 47 | return 1; 48 | } else if (b[prop1] === undefined) { 49 | return -1; 50 | } else if (a[prop1] === b[prop1]) { 51 | var sortStatus = 0; 52 | if (a[prop2].toString().toLowerCase() < b[prop2].toString().toLowerCase()) { 53 | sortStatus = -1; 54 | } else if (String(a[prop2]).toString().toLowerCase() > b[prop2].toString().toLowerCase()) { 55 | sortStatus = 1; 56 | } 57 | } else { 58 | if (a[prop1].toString().toLowerCase() < b[prop1].toString().toLowerCase()) { 59 | sortStatus = -1; 60 | } else { 61 | sortStatus = 1; 62 | } 63 | }; 64 | return sortStatus; 65 | }; 66 | } 67 | 68 | formatInt (number) { 69 | let str = number.toLocaleString('en-US'); 70 | str = str.replace(/,/g, ' '); 71 | str = str.replace(/\./, ','); 72 | return str; 73 | } 74 | 75 | rmDir (dirPath, extension, removeSelf) { 76 | if (removeSelf === undefined) 77 | removeSelf = true; 78 | try { 79 | var files = fs.readdirSync(dirPath); 80 | } catch (e) /* istanbul ignore next */ { 81 | return false; 82 | } 83 | 84 | if (files.length > 0) 85 | for (let i = 0; i < files.length; i++) { 86 | let filePath = dirPath + '/' + files[i]; 87 | /* istanbul ignore else */ 88 | if (fs.statSync(filePath).isFile()) { 89 | if (extension !== null) { 90 | if (path.extname(filePath) == extension) 91 | fs.unlinkSync(filePath); 92 | } else { 93 | fs.unlinkSync(filePath); 94 | } 95 | 96 | } else { 97 | utils.rmDir(filePath); 98 | } 99 | } 100 | /* istanbul ignore else */ 101 | if (removeSelf) 102 | fs.rmdirSync(dirPath); 103 | 104 | return true; 105 | }; 106 | 107 | capitalize (string) { 108 | return string.charAt(0).toUpperCase() + string.slice(1); 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sfdc-generate-data-dictionary", 3 | "version": "1.2.15", 4 | "description": "Generate data dictionary from a Salesforce Org", 5 | "main": "index.js", 6 | "bin": { 7 | "sgd": "./bin/cli" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/gavignon/sfdc-generate-data-dictionary.git" 15 | }, 16 | "keywords": [ 17 | "salesforce", 18 | "data-dictionary" 19 | ], 20 | "author": "Gil Avignon ", 21 | "license": "MIT", 22 | "dependencies": { 23 | "bytes": "^2.5.0", 24 | "commander": "^2.11.0", 25 | "excel4node": "^1.2.1", 26 | "jsforce": "^1.8.0" 27 | } 28 | } 29 | --------------------------------------------------------------------------------