├── .env ├── package.json ├── index.js ├── LICENSE ├── .gitignore ├── README.md ├── ocapi-calls.js ├── webdav-calls.js └── form-file-util.js /.env: -------------------------------------------------------------------------------- 1 | BASE_URL=https://dev00-store-X.demandware.net 2 | CLIENT_ID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 3 | CLIENT_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 4 | BM_USER=the.user 5 | BM_PASS=the$Pass 6 | SITE_ID=The-Site 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sfcc-form-fields-to-csv", 3 | "version": "0.0.1", 4 | "description": "A tool to collect sites form field info from XML files and export them in one CSV file. While gathering info considers cartridge path and site locales.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Serdar Büyüktemiz", 10 | "license": "MIT", 11 | "dependencies": { 12 | "dotenv": "^6.2.0", 13 | "fs": "0.0.1-security", 14 | "request": "^2.87.0", 15 | "request-promise": "^4.2.2", 16 | "webdav": "^2.2.0", 17 | "xml2js": "^0.4.19" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { load } = require('dotenv'); 4 | 5 | const ocapi = require('./ocapi-calls'); 6 | const webdav = require('./webdav-calls'); 7 | const formFileUtil = require('./form-file-util'); 8 | 9 | load(); 10 | 11 | (async ({ BASE_URL, BM_USER, BM_PASS, CLIENT_ID, CLIENT_KEY, SITE_ID }) => { 12 | try { 13 | const token = await ocapi.getToken(BASE_URL, BM_USER, BM_PASS, CLIENT_ID, CLIENT_KEY); 14 | const activeCodeVersion = await ocapi.getActiveCodeVersion(BASE_URL, token); 15 | const cartridges = await ocapi.getSiteCartridges(BASE_URL, SITE_ID, token); 16 | const locales = await ocapi.getSiteLocales(BASE_URL, SITE_ID, token); 17 | 18 | await webdav.getFormXMLsOfSiteCartridges(BASE_URL, BM_USER, BM_PASS, activeCodeVersion, cartridges, locales); 19 | await formFileUtil.processFiles(); 20 | 21 | console.log('\r\ncompleted!\r\n'); 22 | } 23 | catch(err) { 24 | console.error(err); 25 | process.exit(1); 26 | } 27 | })(dotenv.config().parsed); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Serdar Büyüktemiz 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | files/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # next.js build output 64 | .next 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sfcc-form-fields-to-csv 2 | A tool to collect sites form field info from XML files and export them in one CSV file. While gathering info considers cartridge path and site locales. 3 | 4 | *Please set the settings in the settings in .env file before running the tool.* 5 | 6 | ### OCAPI Roles & Permissions 7 | 8 |

 9 | {
10 |     "client_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
11 |     "resources": [{
12 |                     "resource_id": "/code_versions",
13 |                     "methods": ["get"],
14 |                     "read_attributes": "(**)",
15 |                     "write_attributes": "(**)"
16 |                 },
17 |                 {
18 |                     "resource_id": "/sites/**",
19 |                     "methods": ["get"],
20 |                     "read_attributes": "(**)",
21 |                     "write_attributes": "(**)"
22 |                 }]
23 | }
24 | 
25 | 26 | ### How to run this tool? 27 | 28 | you should have git, node and npm installed so that you can run the tool with these commands. 29 | 30 |

31 |  git clone https://github.com/serdarb/sfcc-form-fields-to-csv.git
32 |  cd  .\sfcc-field-info-export\
33 |  npm install
34 |  node .\index.js
35 |  
36 | 37 | -------------------------------------------------------------------------------- /ocapi-calls.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request-promise'); 4 | 5 | const getToken = async (baseURL, bmUser, bmPass, clientId, clientKey) => { 6 | 7 | let response = await request.post(`${baseURL}/dw/oauth2/access_token`, { 8 | headers: { 9 | 'Authorization': 'Basic ' + Buffer.from(`${bmUser}:${bmPass}:${clientKey}`).toString('base64'), 10 | 'Content-Type': 'application/x-www-form-urlencoded' 11 | }, 12 | form: { 'grant_type': 'urn:demandware:params:oauth:grant-type:client-id:dwsid:dwsecuretoken' }, 13 | qs: { 'client_id': clientId }, 14 | json: true 15 | }); 16 | 17 | let access_token = response.access_token; 18 | if(!access_token) { 19 | throw new Error('(getToken) access_token is empty, response: ' + JSON.stringify(response)); 20 | } 21 | else { 22 | return access_token; 23 | } 24 | }; 25 | 26 | const getActiveCodeVersion = async (baseURL, token) => { 27 | 28 | let response = await request.get(`${baseURL}/s/-/dw/data/v19_1/code_versions`, { 29 | headers: { 30 | 'Authorization': 'Bearer ' + token, 31 | 'Content-Type': 'application/json' 32 | }, 33 | json: true 34 | }); 35 | 36 | let activeCodeVersion = response.data.filter(x => x.active)[0].id; 37 | if(!activeCodeVersion) { 38 | throw new Error('(getActiveCodeVersion) activeCodeVersion is empty, response: ' + JSON.stringify(response)); 39 | } 40 | else { 41 | return activeCodeVersion; 42 | } 43 | }; 44 | 45 | const getSiteCartridges = async (baseURL, siteId, token) => { 46 | 47 | let response = await request.get(`${baseURL}/s/-/dw/data/v19_1/sites/${siteId}`, { 48 | headers: { 49 | 'Authorization': 'Bearer ' + token, 50 | 'Content-Type': 'application/json' 51 | }, 52 | json: true 53 | }); 54 | 55 | let cartridges = response.cartridges.split(':'); 56 | if(!cartridges) { 57 | throw new Error('(getSiteCartridges) cartridges is empty, response: ' + JSON.stringify(response)); 58 | } 59 | else { 60 | return cartridges; 61 | } 62 | }; 63 | 64 | const getSiteLocales = async (baseURL, siteId, token) => { 65 | 66 | let response = await request.get(`${baseURL}/s/-/dw/data/v19_1/sites/${siteId}/locale_info/locales`, { 67 | headers: { 68 | 'Authorization': 'Bearer ' + token, 69 | 'Content-Type': 'application/json' 70 | }, 71 | json: true 72 | }); 73 | 74 | let locales = response.hits; 75 | if(!locales) { 76 | throw new Error('(getSiteLocales) locales is empty, response: ' + JSON.stringify(response)); 77 | } 78 | else { 79 | 80 | var result = []; 81 | 82 | locales.forEach(locale => { 83 | var idInfo = locale.id.split('-'); 84 | var language = idInfo[0]; 85 | var country = idInfo[1]; 86 | result.push({'language':language, 'country':country}); 87 | }); 88 | 89 | return result; 90 | } 91 | }; 92 | 93 | module.exports = { 94 | getToken : getToken, 95 | getActiveCodeVersion : getActiveCodeVersion, 96 | getSiteCartridges : getSiteCartridges, 97 | getSiteLocales : getSiteLocales 98 | }; 99 | -------------------------------------------------------------------------------- /webdav-calls.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { createClient } = require('webdav'); 4 | const fs = require('fs'); 5 | 6 | const getFormFilesIfPathExists = async (client, path, locales, foundFormXMlFileNames, localeFolderName) => { 7 | let foundFiles = []; 8 | 9 | for (let j = 0, jj = locales.length; j !== jj; j++) { 10 | try { 11 | let contents = await client.getDirectoryContents(path + '/' + locales[j]); 12 | 13 | for (let i = 0, ii = contents.length; i !== ii; i++) { 14 | if (contents[i].filename.indexOf('xml') !== -1) { 15 | let filePath = contents[i].filename; 16 | let fileName = filePath.substring(filePath.lastIndexOf('/') + 1); 17 | 18 | if (!foundFormXMlFileNames.includes(fileName)) { 19 | foundFiles.push(filePath); 20 | foundFormXMlFileNames.push(fileName); 21 | 22 | fs.appendFileSync(localeFolderName + '\\fullpaths.txt', fileName + ',' + path + '/' + locales[j] + '/' + fileName + '\r\n'); 23 | } 24 | } else { 25 | let val = await getFormFilesIfPathExists(client, contents[i].filename, foundFormXMlFileNames, localeFolderName); 26 | foundFiles = foundFiles.concat(val); 27 | } 28 | } 29 | } catch (e) { 30 | if (!e.response) 31 | { 32 | console.log(e); 33 | } 34 | else if (e.response.status !== 404) { 35 | console.log(path + '\r\n' + e + '\r\n'); 36 | } 37 | } 38 | } 39 | 40 | return foundFiles; 41 | } 42 | 43 | const getFormXMLsOfSiteCartridges = async (baseURL, bmUser, bmPass, activeCodeVersion, cartridges, locales) => { 44 | console.log('starting to get form xmls\r\n'); 45 | 46 | let client = createClient(baseURL + '/on/demandware.servlet/webdav/Sites/Cartridges/', { username: bmUser, password: bmPass }); 47 | 48 | let preparedLocales = locales.map(locale => [locale.language + '_' + locale.country, locale.language, 'default']); 49 | let paths = cartridges.map(cartridge => '/' + activeCodeVersion + '/' + cartridge + '/cartridge/forms'); 50 | 51 | for (let j = 0, jj = preparedLocales.length; j !== jj; j++) { 52 | let foundFromXMLFilePaths = []; 53 | let foundFormXMlFileNames = []; 54 | 55 | var localeName = preparedLocales[j][1]; 56 | var localeFolderName = __dirname + '\\files\\' + localeName; 57 | if (!fs.existsSync(localeFolderName)) { 58 | fs.mkdirSync(localeFolderName, { recursive: true }, (r) => { if (r != null) { console.log(r); } }); 59 | } 60 | 61 | for (let i = 0, ii = paths.length; i !== ii; i++) { 62 | foundFromXMLFilePaths = foundFromXMLFilePaths.concat(await getFormFilesIfPathExists(client, paths[i], preparedLocales[j], foundFormXMlFileNames, localeFolderName)); 63 | } 64 | 65 | console.log('\r\n' + foundFromXMLFilePaths.length + ' form found for ' + localeName + ' locale\r\n'); 66 | 67 | for (let i = 0, ii = foundFromXMLFilePaths.length; i !== ii; i++) { 68 | var formXMLContent = await client.getFileContents(foundFromXMLFilePaths[i], { format: 'text' }); 69 | let fileName = foundFromXMLFilePaths[i].substring(foundFromXMLFilePaths[i].lastIndexOf('/') + 1); 70 | 71 | await fs.writeFile(localeFolderName + '\\' + fileName, formXMLContent, (r) => { if (r != null) { console.log(r); } }); 72 | } 73 | } 74 | }; 75 | 76 | module.exports = { 77 | getFormXMLsOfSiteCartridges: getFormXMLsOfSiteCartridges 78 | }; 79 | -------------------------------------------------------------------------------- /form-file-util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const xmlParser = require('xml2js').parseString; 5 | 6 | const filesFolder = __dirname + '\\files'; 7 | 8 | const processFiles = () => { 9 | 10 | fs.readdir(filesFolder, (err, localeFolders) => { 11 | if (localeFolders == null) { return; } 12 | 13 | for (var i = 0; i < localeFolders.length; i++) { 14 | let localeFolder = localeFolders[i]; 15 | 16 | let localeFolderPath = filesFolder + '\\' + localeFolder; 17 | fs.readdir(localeFolderPath, (err, fileNames) => { 18 | if (fileNames == null) { return; } 19 | 20 | let fullPathFileArray = fs.readFileSync(localeFolderPath + '\\fullpaths.txt').toString().split("\r\n"); 21 | let fullPaths = fullPathFileArray.map(p => { var items = p.split(','); 22 | return { 'key': items[0], 'value': items[1] }; }); 23 | 24 | for (let j = 0; j < fileNames.length; j++) { 25 | let fileName = fileNames[j]; 26 | if(!fileName.endsWith('xml')) { 27 | continue; 28 | } 29 | 30 | let fileNamePretty = fileName.replace('.xml', ''); 31 | let fullPath = fullPaths.filter(x => x.key === fileName)[0].value; 32 | 33 | fs.readFile(localeFolderPath + '\\' + fileName, 'utf8', (err, xmlContent) => { 34 | xmlParser(xmlContent, (err, xmlObject) => { 35 | 36 | let fields = xmlObject.form.field; 37 | if (fields) { 38 | fields.forEach(fld => { getFieldInfo(fullPath, localeFolder, fileNamePretty, fld); }); 39 | } 40 | 41 | let groups = xmlObject.form.group; 42 | if (groups) { 43 | groups.forEach(group => { getGroupFields(fullPath, localeFolder, fileNamePretty, group); }); 44 | } 45 | 46 | let lists = xmlObject.form.list; 47 | if (lists) { 48 | lists.forEach(list => { getListFields(fullPath, localeFolder, fileNamePretty, list); }); 49 | } 50 | }); 51 | }); 52 | } 53 | }); 54 | } 55 | }); 56 | } 57 | 58 | const getGroupFields = async (path, locale, fileName, group, prefix) => { 59 | let groupFields = group.field; 60 | if (!groupFields) { return; } 61 | 62 | prefix = prefix != null || prefix !== undefined ? prefix + '.' + group.$['formid'] : group.$['formid']; 63 | 64 | groupFields.forEach(groupField => { getFieldInfo(path, locale, fileName, groupField, prefix); }); 65 | 66 | let groupGroups = group.group; 67 | if (groupGroups) { 68 | groupGroups.forEach(grp => { getGroupFields(path, locale, fileName, grp, prefix); }); 69 | } 70 | } 71 | 72 | const getListFields = async (path, locale, fileName, list, prefix) => { 73 | let listFields = list.field; 74 | if (!listFields) { return; } 75 | 76 | prefix = prefix != null || prefix !== undefined ? prefix + '.' + list.$['formid'] : list.$['formid']; 77 | 78 | listFields.forEach(listField => { getFieldInfo(path, locale, fileName, listField, prefix); }); 79 | 80 | let listLists = list.list; 81 | if (listLists) { 82 | listLists.forEach(lst => { getListFields(path, locale, fileName, lst, prefix); }); 83 | } 84 | 85 | let listGroups = list.group; 86 | if (listGroups) { 87 | listGroups.forEach(grp => { getGroupFields(path, locale, fileName, grp, prefix); }); 88 | } 89 | } 90 | 91 | const getFieldInfo = async (path, locale, fileName, field, prefix) => { 92 | let info = { 93 | 'path': path, 94 | 'locale': locale, 95 | 'fileName': fileName, 96 | 'name': prefix != null ? prefix + '.' + field.$['formid'] : field.$['formid'], 97 | 'mandatory': field.$['mandatory'] || '', 98 | 'regexp': field.$['regexp'] || '', 99 | 'max-length': field.$['max-length'] || '', 100 | 'min-length': field.$['min-length'] || '' 101 | }; 102 | //let infoObjectString = JSON.stringify(info); 103 | 104 | fs.appendFileSync(filesFolder + '\\export-' + locale + '.csv', 105 | info.fileName + ',' + 106 | info.name + ',' + 107 | info.mandatory + ',' + 108 | info.regexp + ',' + 109 | info['max-length'] + ',' + 110 | info['min-length'] + ','+ 111 | info.path + 112 | '\r\n'); 113 | return info; 114 | } 115 | 116 | module.exports = { 117 | processFiles : processFiles, 118 | getFieldInfo : getFieldInfo, 119 | getListFields : getListFields, 120 | getGroupFields : getGroupFields 121 | }; 122 | --------------------------------------------------------------------------------