@ds-sfdc/sfparty 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ 'use strict'
2
+ import * as fs from 'fs'
3
+ import * as path from 'path'
4
+ import * as yaml from 'js-yaml'
5
+
6
+ export function directoryExists(dirPath) {
7
+ return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()
8
+ }
9
+
10
+ export function fileExists(filePath) {
11
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile()
12
+ }
13
+
14
+ export function createDirectory(dirPath) {
15
+ if (!fs.existsSync(dirPath)) {
16
+ fs.mkdirSync(dirPath, { recursive: true })
17
+ }
18
+ }
19
+
20
+ export function deleteDirectory(dirPath, recursive = false) {
21
+ if (!directoryExists(dirPath)) {
22
+ return false
23
+ } else {
24
+ if (fs.existsSync(dirPath)) {
25
+ fs.readdirSync(dirPath).forEach(function (file) {
26
+ var curPath = path.join(dirPath, file)
27
+ if (fs.lstatSync(curPath).isDirectory() && recursive) { // recurse
28
+ deleteDirectory(curPath, recursive);
29
+ } else { // delete file
30
+ fs.unlinkSync(curPath);
31
+ }
32
+ })
33
+ return fs.rmdirSync(dirPath);
34
+ }
35
+ }
36
+ }
37
+
38
+ export function getFiles(dirPath, filter = undefined) {
39
+ const filesList = []
40
+ fs.readdirSync(dirPath).forEach(file => {
41
+ if (!filter) {
42
+ filesList.push(file)
43
+ } else {
44
+ if (file.endsWith(filter)) {
45
+ filesList.push(file)
46
+ }
47
+ }
48
+ })
49
+ filesList.sort()
50
+ return filesList
51
+ }
52
+
53
+ export function getDirectories(dirPath) {
54
+ return fs.readdirSync(dirPath, { withFileTypes: true })
55
+ .filter(dirent => dirent.isDirectory())
56
+ .map(dirent => dirent.name)
57
+ }
58
+
59
+ export function deleteFile(filePath) {
60
+ if (!fileExists(filePath)) {
61
+ return false
62
+ } else {
63
+ return unlinkSync(filePath, { recursive: false, force: true });
64
+ }
65
+ }
66
+
67
+ export function fileInfo(filePath) {
68
+ return {
69
+ "dirname": path.join(path.dirname(filePath)), //something/folder/example
70
+ "basename": path.basename(filePath, path.extname(filePath)), //example
71
+ "filename": path.basename(filePath), //example.txt
72
+ "extname": path.extname(filePath), //txt
73
+ "exists": fs.existsSync(filePath), //true if exists or false if not exists
74
+ "stats": fs.existsSync(filePath) ? fs.statSync(filePath) : undefined //stats object if exists or undefined if not exists
75
+ }
76
+ }
77
+
78
+ export function savePartFile(json, fileName, format) {
79
+ try {
80
+ switch (format) {
81
+ case 'json':
82
+ let jsonString = JSON.stringify(json, null, '\t')
83
+ fs.writeFileSync(fileName, jsonString)
84
+ break
85
+ case 'yaml':
86
+ let doc = yaml.dump(json)
87
+ fs.writeFileSync(fileName, doc)
88
+ break
89
+ }
90
+ return true
91
+ } catch (error) {
92
+ global.logger.error(error)
93
+ throw error
94
+ }
95
+ }
96
+
97
+ export function readPartFile(fileName) {
98
+ try {
99
+ let result = undefined
100
+ const data = fs.readFileSync(fileName, { encoding: 'utf8', flag: 'r' })
101
+ if (fileName.indexOf('.yaml') != -1) {
102
+ result = yaml.load(data)
103
+ } else {
104
+ result = JSON.parse(data)
105
+ }
106
+ return result
107
+ } catch (error) {
108
+ global.logger.error(error)
109
+ throw error
110
+ }
111
+ }
112
+
113
+ export function writeFile(fileName, data, atime = new Date(), mtime = new Date()) {
114
+ try {
115
+ // write data to the file
116
+ fs.writeFileSync(fileName, data)
117
+
118
+ // if atime or mtime are undefined, use current date/time
119
+ if (atime === undefined) atime = new Date()
120
+ if (mtime === undefined) mtime = new Date()
121
+
122
+ // update XML file to match the latest atime and mtime of the files processed
123
+ fs.utimesSync(fileName, atime, mtime)
124
+
125
+ } catch (error) {
126
+ global.logger.error(error)
127
+ throw error
128
+ }
129
+ }
130
+
131
+ export function find(filename, root) {
132
+ // code Copyright (c) 2014, Ben Gourley
133
+ // https://github.com/bengourley/find-nearest-file
134
+ root = root || process.cwd();
135
+
136
+ if (!filename) throw new Error('filename is required')
137
+
138
+ if (filename.indexOf('/') !== -1 || filename === '..') {
139
+ throw new Error('filename must be just a filename and not a path')
140
+ }
141
+
142
+
143
+ function findFile(directory, filename) {
144
+
145
+ var file = path.join(directory, filename)
146
+
147
+ try {
148
+ if (fs.statSync(file).isFile()) return file
149
+ // stat existed, but isFile() returned false
150
+ return nextLevelUp()
151
+ } catch (e) {
152
+ // stat did not exist
153
+ return nextLevelUp()
154
+ }
155
+
156
+ function nextLevelUp() {
157
+ // Don't proceed to the next directory when already at the fs root
158
+ if (directory === path.resolve('/')) return null
159
+ return findFile(path.dirname(directory), filename)
160
+ }
161
+
162
+ }
163
+
164
+ return findFile(root, filename)
165
+ }
@@ -0,0 +1,203 @@
1
+ import path from 'path'
2
+ import os from 'os'
3
+ import { readFileSync, writeFileSync, utimesSync } from 'fs'
4
+ import logUpdate from 'log-update'
5
+ import chalk from 'chalk'
6
+ import convertHrtime from 'convert-hrtime'
7
+ import cliSpinners from 'cli-spinners'
8
+ import * as fileUtils from '../fileUtils.js'
9
+ import { labelDefinition } from './definition.js'
10
+ import * as xml2js from 'xml2js'
11
+ import * as yaml from 'js-yaml'
12
+
13
+ const spinner = cliSpinners['dots']
14
+
15
+ export class CustomLabel {
16
+ #xml = ''
17
+ #types = []
18
+ #spinnerMessage = ''
19
+ #index = 0
20
+ #startTime = 0
21
+ #fileName = ''
22
+ #errorMessage = ''
23
+ #fileStats
24
+
25
+ constructor(config) {
26
+ this.sourceDir = config.sourceDir
27
+ this.targetDir = config.targetDir
28
+ this.metaDir = config.metaDir
29
+ this.processList = config.processList
30
+ }
31
+
32
+ combine() {
33
+ return new Promise((resolve, reject) => {
34
+ const that = this
35
+ if (!fileUtils.directoryExists(that.sourceDir)) reject(`Path does not exist: ${that.sourceDir}`)
36
+
37
+ that.#xml = `<?xml version="1.0" encoding="UTF-8"?>${os.EOL}`
38
+ that.#xml += `<CustomLabels xmlns="https://soap.sforce.com/2006/04/metadata">${os.EOL}`
39
+
40
+ labelDefinition.directories.forEach(key => { that.#types.push(key) })
41
+ that.#types.sort()
42
+
43
+ setFileName(that)
44
+ processLabel(that)
45
+
46
+ saveXML(that)
47
+ resolve(true)
48
+ })
49
+
50
+ function setFileName(that) {
51
+ that.#fileName = path.join(that.targetDir, 'CustomLabels.labels-meta.xml')
52
+ }
53
+
54
+ function processLabel(that) {
55
+ that.#startTime = process.hrtime.bigint()
56
+ that.#spinnerMessage = `[%1] of ${global.processed.total} - Custom Label: [%4]${chalk.yellowBright('[%5]')}[%2][%3]`
57
+ that.processList.sort()
58
+ that.#types.forEach(key => {
59
+ processDirectory(that, key)
60
+ })
61
+ }
62
+
63
+ function processFile(that, key) {
64
+ const fileName = path.join(that.sourceDir, that.metaDir, `${key}.${global.format}`)
65
+ if (fileUtils.fileExists(fileName)) {
66
+ if (labelDefinition.singleFiles.includes(key)) {
67
+ genericXML(that, key)
68
+ } else {
69
+ that.#errorMessage += `\n${global.statusLevel.warn} Not processed: ${key}`
70
+ }
71
+ }
72
+ }
73
+
74
+ function processDirectory(that, key) {
75
+ that.sequence = 0
76
+ let startTime
77
+ that.#errorMessage = ''
78
+ that.processList.forEach(fileName => {
79
+ startTime = process.hrtime.bigint()
80
+ that.sequence++
81
+ logUpdate(that.#spinnerMessage
82
+ .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
83
+ .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key}`)
84
+ .replace('[%3]', `${that.#errorMessage}`)
85
+ .replace('[%4]', `${global.statusLevel.working} `)
86
+ .replace('[%5]', `${fileName} `)
87
+ )
88
+ try {
89
+ genericXML(that, key, fileName)
90
+ } catch (error) {
91
+ that.#errorMessage = error.message
92
+ }
93
+ let executionTime = getTimeDiff(BigInt(startTime))
94
+ let durationMessage = `${executionTime.seconds}.${executionTime.milliseconds}s`
95
+ let stateIcon = (that.#errorMessage == '') ? global.statusLevel.success : global.statusLevel.fail
96
+ logUpdate(that.#spinnerMessage
97
+ .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
98
+ .replace('[%2]', `. Processed in ${durationMessage}.`)
99
+ .replace('[%3]', `${that.#errorMessage}`)
100
+ .replace('[%4]', `${stateIcon} `)
101
+ .replace('[%5', fileName)
102
+ )
103
+ logUpdate.done()
104
+ })
105
+ // TODO that.#fileStats = fileUtils.fileInfo(fileName).stats
106
+
107
+ // genericDirectoryXML(that, key)
108
+ // that.#errorMessage += `\n${global.statusLevel.warn} Not processed: ${key}`
109
+ return true
110
+ }
111
+
112
+ function getTimeDiff(startTime, endTime = process.hrtime.bigint()) {
113
+ const diff = BigInt(endTime) - BigInt(startTime)
114
+ let executionTime = convertHrtime(diff)
115
+ executionTime.seconds = Math.round(executionTime.seconds)
116
+ executionTime.milliseconds = Math.round(executionTime.milliseconds / 1000)
117
+ if (executionTime.milliseconds == 0 && executionTime.nanoseconds > 0) executionTime.milliseconds = 1
118
+ return executionTime
119
+ }
120
+
121
+ function saveXML(that) {
122
+ fileUtils.createDirectory(that.targetDir)
123
+ that.#xml += '</CustomLabels>\n'
124
+ writeFileSync(that.#fileName, that.#xml)
125
+ // utimesSync(that.#fileName, that.#fileStats.atime, that.#fileStats.mtime)
126
+
127
+ }
128
+
129
+ function nextFrame(that) {
130
+ return spinner.frames[that.#index = ++that.#index % spinner.frames.length]
131
+ }
132
+
133
+ function genericXML(that, key, fileName) {
134
+ fileName = path.join(that.sourceDir, fileName)
135
+ const builder = new xml2js.Builder({ cdata: false, headless: true, rootName: key })
136
+ if (fileUtils.fileExists(fileName)) {
137
+ const data = readFileSync(fileName, { encoding: 'utf8', flag: 'r' })
138
+ const result = (global.format == 'yaml') ? yaml.load(data) : JSON.parse(data)
139
+ result[key.slice(0, -1)][labelDefinition.sortKeys[key]] = result[labelDefinition.sortKeys[key]]
140
+ result[key.slice(0, -1)] = sortJSONKeys(sortJSON(result[key.slice(0, -1)], labelDefinition.sortKeys[key]))
141
+ that.#xml += `\t<${key}>${os.EOL}`
142
+ Object.keys(result[key.slice(0, -1)]).forEach(tag => {
143
+ let xml
144
+ try {
145
+ xml = builder.buildObject(result[key.slice(0, -1)][tag]).replace(`<${key}>`, '').replace(`</${key}>`, '')
146
+ that.#xml += `\t\t<${tag}>${xml}</${tag}>${os.EOL}`
147
+ } catch (error) {
148
+ global.logger.error(error)
149
+ }
150
+ })
151
+ that.#xml += `\t</${key}>${os.EOL}`
152
+ }
153
+ }
154
+ // end of functions
155
+ // end of combine
156
+ }
157
+
158
+ // end of class
159
+ }
160
+
161
+ function sortJSON(json, key) {
162
+ if (Array.isArray(json)) {
163
+ json.sort((a, b) => {
164
+ if (a[key] < b[key]) return -1
165
+ if (a[key] > b[key]) return 1
166
+ return 0
167
+ })
168
+ }
169
+ return json
170
+ }
171
+
172
+ function sortJSONKeys(json) {
173
+ // sort json keys alphabetically
174
+ if (Array.isArray(json)) {
175
+ json.forEach(function (part, index) {
176
+ this[index] = Object.keys(this[index])
177
+ .sort((a, b) => {
178
+ if (a < b) return -1
179
+ if (a > b) return 1
180
+ return 0
181
+ })
182
+ .reduce((accumulator, key) => {
183
+ accumulator[key] = this[index][key]
184
+
185
+ return accumulator
186
+ }, {})
187
+ }, json)
188
+
189
+ } else {
190
+ json = Object.keys(json)
191
+ .sort((a, b) => {
192
+ if (a < b) return -1
193
+ if (a > b) return 1
194
+ return 0
195
+ })
196
+ .reduce((accumulator, key) => {
197
+ accumulator[key] = json[key]
198
+
199
+ return accumulator
200
+ }, {})
201
+ }
202
+ return json
203
+ }
@@ -0,0 +1,12 @@
1
+ export const labelDefinition = {
2
+ metaUrl: 'https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_customlabels.htm',
3
+ directories: [
4
+ 'labels',
5
+ ],
6
+ sortKeys: {
7
+ 'labels': 'fullName',
8
+ },
9
+ keyOrder: {
10
+ labels: ['fullName', 'shortDescription', 'categories', 'protected', 'language', 'value'],
11
+ },
12
+ }
@@ -0,0 +1,213 @@
1
+ 'use strict'
2
+
3
+ import path from 'path'
4
+ import fs from 'fs'
5
+ import os from 'os'
6
+ import { readFile } from 'fs'
7
+ import { Parser } from 'xml2js'
8
+ import logUpdate from 'log-update'
9
+ import chalk from 'chalk'
10
+ import convertHrtime from 'convert-hrtime'
11
+ import cliSpinners from 'cli-spinners'
12
+ import * as yaml from 'js-yaml'
13
+ import * as fileUtils from '../fileUtils.js'
14
+ import { labelDefinition } from './definition.js'
15
+
16
+ const spinner = cliSpinners['dots']
17
+
18
+ export class CustomLabel {
19
+ #fileName = {
20
+ 'fullName': undefined,
21
+ 'shortName': undefined,
22
+ }
23
+ #json
24
+ #errorMessage = ''
25
+ #index = 0
26
+ #startTime = 0
27
+ #spinnerMessage = ''
28
+
29
+ constructor(config) {
30
+ this.sourceDir = config.sourceDir
31
+ this.targetDir = config.targetDir
32
+ this.metaFilePath = config.metaFilePath
33
+ this.sequence = config.sequence
34
+ }
35
+
36
+ get metaFilePath() {
37
+ return this._metaFilePath
38
+ }
39
+
40
+ set metaFilePath(value) {
41
+ value = value.trim()
42
+ if (value === '') {
43
+ throw 'The file path cannot be empty'
44
+ }
45
+ this._metaFilePath = value
46
+ this.#fileName.fullName = fileUtils.fileInfo(value).filename
47
+ this.#fileName.shortName = fileUtils.fileInfo(value).filename.replace('.labels-meta.xml', '')
48
+ }
49
+
50
+ split() {
51
+ const that = this
52
+ return new Promise((resolve, reject) => {
53
+
54
+ if (!that.#fileName || !that.sourceDir || !that.targetDir || !that.metaFilePath) {
55
+ global.logger.error('Invalid information passed to split')
56
+ process.exit(1)
57
+ }
58
+ if (!fileUtils.fileExists(that.metaFilePath)) {
59
+ global.logger.error(`file not found: ${that.metaFilePath}`)
60
+ process.exit(1)
61
+ }
62
+
63
+ // that.targetDir = path.join(that.targetDir, that.#fileName.shortName)
64
+ let parser = new Parser()
65
+ const getJSON = new Promise((resolve, reject) => {
66
+ readFile(that.metaFilePath, function (err, data) {
67
+ parser.parseString(data, function (err, result) {
68
+ if (result) {
69
+ resolve(result)
70
+ } else {
71
+ global.logger.error(`error converting xml to json: ${that.metaFilePath}`)
72
+ process.exit(1)
73
+ }
74
+ })
75
+ })
76
+ })
77
+ getJSON.then((result) => {
78
+ // modify the json to remove unwanted arrays
79
+ delete result.CustomLabels['$']
80
+ let jsonString = JSON.stringify(result, (name, value) => {
81
+ if (name == '' || !isNaN(name) || labelDefinition.directories.includes(name)) {
82
+ return value
83
+ } else {
84
+ return xml2json(value)
85
+ }
86
+ })
87
+ that.#json = JSON.parse(jsonString)
88
+
89
+ Object.keys(that.#json.CustomLabels).forEach(key => {
90
+ const keyOrder = labelDefinition.keyOrder[key]
91
+ const sortKey = labelDefinition.sortKeys[key]
92
+
93
+ if (Array.isArray(that.#json.CustomLabels[key])) {
94
+ // sort json to order by sortKey
95
+ that.#json.CustomLabels[key].sort((a, b) => {
96
+ if (a[sortKey] < b[sortKey]) {
97
+ return -1;
98
+ }
99
+ if (a[sortKey] > b[sortKey]) {
100
+ return 1;
101
+ }
102
+ return 0;
103
+ })
104
+
105
+ // sort json keys in specified order
106
+ that.#json.CustomLabels[key].forEach(function (part, index) {
107
+ this[index] = Object.keys(this[index])
108
+ .sort((a, b) => {
109
+ if (keyOrder.indexOf(a) < keyOrder.indexOf(b)) return -1
110
+ if (keyOrder.indexOf(a) > keyOrder.indexOf(b)) return 1
111
+ return 0
112
+ })
113
+ .reduce((accumulator, key) => {
114
+ accumulator[key] = this[index][key]
115
+ return accumulator
116
+ }, {})
117
+ }, that.#json.CustomLabels[key])
118
+ }
119
+ })
120
+
121
+ processFile(that)
122
+ completeFile(that)
123
+ resolve(true)
124
+ })
125
+ })
126
+
127
+ function nextFrame(that) {
128
+ return spinner.frames[that.#index = ++that.#index % spinner.frames.length]
129
+ }
130
+
131
+ function completeFile(that) {
132
+ let executionTime = getTimeDiff(BigInt(that.#startTime))
133
+ let durationMessage = `${executionTime.seconds}.${executionTime.milliseconds}s`
134
+ let stateIcon = (that.#errorMessage == '') ? global.statusLevel.success : global.statusLevel.fail
135
+ logUpdate(that.#spinnerMessage
136
+ .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
137
+ .replace('[%2]', `. Processed in ${durationMessage}.`)
138
+ .replace('[%3]', `${that.#errorMessage}`)
139
+ .replace('[%4]', `${stateIcon} `)
140
+ )
141
+ logUpdate.done()
142
+ }
143
+
144
+ function processFile(that) {
145
+ that.#startTime = process.hrtime.bigint()
146
+ that.#spinnerMessage = `[%1] of ${global.processed.total} - Custom Labels: [%4]${chalk.yellowBright(that.#fileName.shortName)}[%2][%3]`
147
+
148
+ fileUtils.deleteDirectory(that.targetDir, true) // recursive delete existing directory
149
+ fileUtils.createDirectory(that.targetDir) // create directory
150
+
151
+ Object.keys(that.#json.CustomLabels).forEach(key => {
152
+ that.sequence = global.processed.current
153
+ logUpdate(that.#spinnerMessage
154
+ .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
155
+ .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key}`)
156
+ .replace('[%3]', `${that.#errorMessage}`)
157
+ .replace('[%4]', `${global.statusLevel.working} `)
158
+ )
159
+ if (labelDefinition.directories.includes(key)) {
160
+ processDirectory(that, key)
161
+ } else {
162
+ that.#errorMessage += `\n${global.statusLevel.warn} Not processed: ${key}`
163
+ }
164
+ })
165
+
166
+ return true
167
+ }
168
+
169
+ function processDirectory(that, key) {
170
+ const myKey = labelDefinition.sortKeys[key]
171
+
172
+ // populate objects with data per object
173
+ that.#json.CustomLabels[key].forEach(element => {
174
+ let fileName = path.join(that.targetDir, `${element[myKey]}.${global.format}`)
175
+ const labelJSON = {}
176
+ labelJSON[myKey] = element[myKey]
177
+ delete element[myKey]
178
+ labelJSON[key.slice(0, -1)] = element //use slice to remove the s
179
+
180
+ let jsonString = JSON.stringify(labelJSON, null, '\t')
181
+ switch (global.format) {
182
+ case 'json':
183
+ fs.writeFileSync(fileName, jsonString)
184
+ break
185
+ case 'yaml':
186
+ let doc = yaml.dump(JSON.parse(jsonString))
187
+ fs.writeFileSync(fileName, doc)
188
+ break
189
+ }
190
+ })
191
+ }
192
+ }
193
+ }
194
+
195
+ function getTimeDiff(startTime, endTime = process.hrtime.bigint()) {
196
+ const diff = BigInt(endTime) - BigInt(startTime)
197
+ let executionTime = convertHrtime(diff)
198
+ executionTime.seconds = Math.round(executionTime.seconds)
199
+ executionTime.milliseconds = Math.round(executionTime.milliseconds / 1000)
200
+ if (executionTime.milliseconds == 0 && executionTime.nanoseconds > 0) executionTime.milliseconds = 1
201
+ return executionTime
202
+ }
203
+
204
+ function xml2json(currentValue) {
205
+ if (Array.isArray(currentValue)) {
206
+ if (currentValue.length == 1) {
207
+ currentValue = currentValue[0].toString().trim()
208
+ }
209
+ }
210
+ if (currentValue == 'true') currentValue = true
211
+ if (currentValue == 'false') currentValue = false
212
+ return currentValue
213
+ }